Developing classes using the 1C-Bitrix D7 API

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Developing Classes Using the D7 API for 1C-Bitrix

The D7 API is not merely a "new API" layered on top of the old one. It represents a different development philosophy: classes instead of global functions, namespaces instead of CModule, Result objects instead of bool and error strings, Application as the entry point instead of global variables. Developing on D7 means code that does not break when Bitrix updates, can be tested, reused, and read.

Core Concepts of the D7 API

Application — singleton, entry point:

$app = \Bitrix\Main\Application::getInstance();
$connection = $app->getConnection();        // database connection
$request    = $app->getContext()->getRequest(); // HTTP request
$server     = $app->getContext()->getServer();  // server data

Result objects. Any method that can fail returns a descendant of \Bitrix\Main\Result:

class MyServiceResult extends \Bitrix\Main\Result
{
    private ?array $data = null;

    public function setData(array $data): void
    {
        $this->data = $data;
    }

    public function getData(): ?array
    {
        return $this->data;
    }
}

// Usage
$result = new MyServiceResult();
if ($someConditionFailed) {
    $result->addError(new \Bitrix\Main\Error('No data to process', 'NO_DATA'));
    return $result;
}
$result->setData($processedData);
return $result;

// Calling code
$result = MyService::process($input);
if (!$result->isSuccess()) {
    foreach ($result->getErrors() as $error) {
        echo $error->getMessage(); // No data to process
    }
}

This eliminates the class of errors "function returned false, but the reason is unknown."

Service Class Structure

Business logic is placed in services — classes with a clear single responsibility:

namespace MyProject\Services;

use Bitrix\Main\Result;
use Bitrix\Main\Error;
use MyProject\Storage\OrderLogTable;

class OrderService
{
    public function __construct(
        private readonly int $orderId
    ) {}

    public function changeStatus(string $newStatus): Result
    {
        $result = new Result();

        $allowedStatuses = ['PENDING', 'PROCESSING', 'SHIPPED', 'DELIVERED', 'CANCELLED'];
        if (!in_array($newStatus, $allowedStatuses, true)) {
            $result->addError(new Error("Invalid status: {$newStatus}", 'INVALID_STATUS'));
            return $result;
        }

        $order = \Bitrix\Sale\Order::load($this->orderId);
        if (!$order) {
            $result->addError(new Error("Order #{$this->orderId} not found", 'ORDER_NOT_FOUND'));
            return $result;
        }

        $saveResult = $order->setField('STATUS_ID', $newStatus);
        if (!$saveResult->isSuccess()) {
            $result->addErrors($saveResult->getErrors());
            return $result;
        }

        $order->save();

        OrderLogTable::add([
            'ORDER_ID' => $this->orderId,
            'ACTION'   => "STATUS_CHANGED_TO_{$newStatus}",
        ]);

        return $result;
    }
}

Working with HTTP Requests via D7

The old way — $_POST['field']. The D7 way:

$request = \Bitrix\Main\Application::getInstance()->getContext()->getRequest();

$field   = $request->getPost('field');     // POST parameter
$param   = $request->getQuery('param');    // GET parameter
$file    = $request->getFile('upload');    // uploaded file
$isAjax  = $request->isAjaxRequest();
$isPost  = $request->isPost();

The Request object filters input data and provides typed access. This is not "magically more secure," but it makes the source of data explicit.

Service Locator and DI

Bitrix does not have a full DI container, but it has a service locator:

// Registering a service (in init.php or in a module method)
\Bitrix\Main\DI\ServiceLocator::getInstance()->addInstanceLazy(
    'myproject.order.service',
    static function() {
        return new \MyProject\Services\OrderNotificationService(
            new \MyProject\Services\MailSender(),
            new \MyProject\Services\SmsGateway()
        );
    }
);

// Retrieving the service anywhere in the code
$notifier = \Bitrix\Main\DI\ServiceLocator::getInstance()
    ->get('myproject.order.service');

Moving away from global variables and static calls makes code testable.

D7 Cache

$cache = \Bitrix\Main\Data\Cache::createInstance();
$cacheKey = md5('product_list_' . $categoryId);

if ($cache->initCache(3600, $cacheKey, '/myproject/products/')) {
    $data = $cache->getVars();
} else {
    $data = $this->loadFromDatabase($categoryId);
    $cache->startDataCache();
    $cache->endDataCache($data);
}

Tagged cache for invalidating a group of records:

$taggedCache = \Bitrix\Main\Application::getInstance()->getTaggedCache();
$taggedCache->startTagCache('/myproject/products/');
$taggedCache->registerTag('my_product_' . $productId);
$taggedCache->endTagCache();

// Invalidation when a product changes
$taggedCache->clearByTag('my_product_' . $productId);

Working with Files via D7

use Bitrix\Main\IO\File;
use Bitrix\Main\IO\Directory;

// Creating a directory
Directory::createDirectory('/path/to/dir');

// Working with a file
$file = new File('/path/to/file.txt');
$file->putContents('content');
$content = $file->getContents();
$exists  = $file->isExists();

Common Patterns When Developing Service Classes

Repository — a class for working with data storage, isolates ORM code from business logic.

Factory — an object factory by type or parameters.

Strategy — algorithm selection at runtime (e.g., different discount calculation methods depending on customer type).

These patterns are not tied to Bitrix — they are standard PHP. D7 simply provides enough tools to apply them.

Timeline

Task Timeline
A set of service classes for a specific feature (5–10 classes) 1–2 weeks
Full service layer for a module (repositories, services, factories, Result objects) 3–5 weeks
Writing unit tests for existing classes +30–50% on top of development time

Code built on the D7 API has a long lifespan. It can be updated incrementally, tested, and reused in other projects. This pays off at the very first major refactoring or when handing the project over to a new developer.