Разработка компонентов PHP: от инфоблоков до пакетов Composer

Раздел: Разработка на PHP -> Компоненты PHP (Bitrix, фреймворки)

Компонент Bitrix на основе D7

Как организовать переиспользуемый компонент с поддержкой параметров и шаблонов в Bitrix?

Наиболее эффективное решение - создание компонента с использованием ядра D7 (namespace Bitrix). Это позволяет четко отделить логику от представления и использовать автозагрузку классов.

Структура папок компонента (например, mynamespace:mycomponent):

/local/components/mynamespace/mycomponent/class.php
/class.php? lang=ru
/templates/.default/template.php
/templates/.default/result_modifier.php
/templates/.default/component_epilog.php
/.parameters.php
/.description.php

Php components (php компоненты)

В class.php определяем класс, наследующий CBitrixComponent:

<?php
namespace Mynamespace;
use Bitrix\Main\Loader;

class MyComponent extends \CBitrixComponent
{
    public function executeComponent()
    {
        if (!Loader::includeModule('iblock'))
        {
            ShowError('Модуль инфоблоков не установлен');
            return;
        }

        // Параметры компонента
        $iblockId = (int)$this->arParams['IBLOCK_ID'];
        if ($iblockId <= 0)
        {
            $this->arResult['ERROR'] = 'Не указан ID инфоблока';
            $this->includeComponentTemplate();
            return;
        }

        // Логика выборки
        $this->arResult['ITEMS'] = [];
        $filter = ['ACTIVE' => 'Y', 'IBLOCK_ID' => $iblockId];
        $select = ['ID', 'NAME', 'PREVIEW_PICTURE', 'DETAIL_PAGE_URL'];
        $res = \CIBlockElement::GetList([], $filter, false, false, $select);
        while ($item = $res->Fetch())
        {
            $this->arResult['ITEMS'][] = $item;
        }

        $this->includeComponentTemplate();
    }
}

Описание (.description.php):

<?php
use Bitrix\Main\Localization\Loc;
Loc::loadMessages(__FILE__);

$arComponentDescription = [
    'NAME' => Loc::getMessage('MYCOMPONENT_NAME'),
    'DESCRIPTION' => Loc::getMessage('MYCOMPONENT_DESC'),
    'PATH' => ['ID' => 'mynamespace'],
    'CACHE_PATH' => 'Y',
    'COMPLEX' => 'N',
];

Параметры (.parameters.php):

<?php
use Bitrix\Main\Localization\Loc;
Loc::loadMessages(__FILE__);

$arComponentParameters = [
    'GROUPS' => [],
    'PARAMETERS' => [
        'IBLOCK_ID' => [
            'NAME' => Loc::getMessage('MYCOMPONENT_IBLOCK_ID'),
            'TYPE' => 'STRING',
            'DEFAULT' => '',
        ],
        'CACHE_TIME' => ['DEFAULT' => 36000000],
    ],
];

Типичная ошибка

Неверное указание namespace в class.php или отсутствие автозагрузки. Компонент не подключается, появляется ошибка "Класс не найден". Решение: проверить регистр букв, соответствие пути и namespace, а также выполнить сброс кеша системы (меню "Настройки" → "Инструменты" → "Сброс кеша").

Как сделать компонент с кэшированием результатов?

Добавить в executeComponent() проверку кэша через CPHPCache или использовать встроенный механизм абстрактного компонента:

if ($this->startResultCache(3600, $this->arParams))
{
    // выборка данных
    $this->includeComponentTemplate();
}
$this->endResultCache();

Компонент в Laravel через Service Provider

Как упаковать функциональность в отдельный пакет (компонент) в Laravel?

Service Provider регистрирует зависимости, маршруты, шаблоны. Создадим провайдер App\Providers\CustomComponentProvider.

<?php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class CustomComponentProvider extends ServiceProvider
{
    public function boot()
    {
        // Публикация миграций
        $this->publishes([
            __DIR__.'/../../database/migrations/' => database_path('migrations'),
        ], 'migrations');

        // Загрузка маршрутов
        $this->loadRoutesFrom(__DIR__.'/../../routes/web.php');

        // Загрузка представлений
        $this->loadViewsFrom(__DIR__.'/../../resources/views', 'custom');
    }

    public function register()
    {
        // Регистрация фасада
        $this->app->bind('custom-helper', function () {
            return new \App\Helpers\CustomHelper();
        });
    }
}

Использование в Blade: @extends('custom::layout'). Регистрация в config/app.php providers.

Проблема

Конфликт поставщиков с одинаковыми псевдонимами. Решение: давать уникальные имена тегам и фасадам, не пересекающиеся с пакетами других авторов.

Symfony Bundle как компонент

Как создать независимый бандл в Symfony?

Структура бандла: src/Controller, src/DependencyInjection, src/Resources/config. Класс AppBundle (пример).

<?php
namespace App\AppBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class AppBundle extends Bundle
{
}

Настройка конфигурации через DependencyInjection Extension.

<?php
namespace App\AppBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;

class AppExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.yaml');
    }
}

Подключение: в config/bundles.php добавить App\AppBundle\AppBundle::class => ['all' => true].

Типичная ошибка

Не указан Extension в файле services.yaml или неверный namespace. Бандл не активируется. Решение: проверить структуру папок и загрузку Extension в container builder.

Расширенные примеры создания PHP компонентов с детальными пояснениями и выводом результатов.

Пример 1: Компонент Bitrix с фильтрацией и пагинацией

Создадим компонент mynamespace:itemlist, который выводит элементы инфоблока с постраничной навигацией и фильтром по свойству.

Пример
<?php
namespace Mynamespace;

use Bitrix\Main\Application;
use Bitrix\Main\Type\DateTime;
use Bitrix\Iblock\Component\Base;

class Itemlist extends \CBitrixComponent
{
    public function executeComponent()
    {
        $request = Application::getInstance()->getContext()->getRequest();
        $navParams = (new \Bitrix\Main\UI\PageNavigation('page'))
            ->allowAllRecords(true)
            ->setPageSize($this->arParams['PAGE_SIZE'] ?: 10)
            ->initFromUri();

        $filter = ['ACTIVE' => 'Y', 'IBLOCK_ID' => $this->arParams['IBLOCK_ID']];
        if (!empty($this->arParams['FILTER_PROP'])) {
            $propCode = $this->arParams['FILTER_PROP'];
            $propValue = $request->getQuery(strtolower($propCode));
            if ($propValue) {
                $filter['PROPERTY_'.$propCode] = $propValue;
            }
        }

        $select = ['ID', 'NAME', 'PROPERTY_*'];
        $res = \CIBlockElement::GetList(
            ['SORT' => 'ASC'],
            $filter,
            false,
            $navParams->getParams(),
            $select
        );

        $this->arResult['ITEMS'] = [];
        while ($item = $res->GetNextElement())
        {
            $fields = $item->GetFields();
            $props = $item->GetProperties();
            $this->arResult['ITEMS'][] = [
                'NAME' => $fields['NAME'],
                'PRICE' => $props['PRICE']['VALUE'] ?? '',
            ];
        }

        $this->arResult['NAV'] = $navParams;
        $this->arResult['NAV_STRING'] = $navParams->getRecordCount() > $navParams->getPageSize() ? $navParams->getPageNavigation() : '';
        $this->includeComponentTemplate();
    }
}

Шаблон template.php (фрагмент):

Пример
<?php if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die(); ?>
<h2>Список товаров</h2>
<?php foreach ($arResult['ITEMS'] as $item): ?>
    <div>Название: <?= $item['NAME'] ?>, Цена: <?= $item['PRICE'] ?></div>
<?php endforeach; ?>
<?= $arResult['NAV_STRING'] ?>

Результат вызова компонента на странице (вид frond-end):

Список товаров
Название: Товар 1, Цена: 100
Название: Товар 2, Цена: 200
Название: Товар 3, Цена: 150
1 2 3 ...

Проблема: Если не передавать параметр PAGE_SIZE, пагинация не отобразится. Решение: задать значение по умолчанию в .parameters.php.

Пример 2: Независимый Composer пакет

Создадим библиотеку для расчёта скидок. Структура:

Пример
composer.json
src/
  DiscountCalculator.php
Пример
{
    "name": "mycompany/discount-calculator",
    "description": "Библиотека для расчёта скидок",
    "type": "library",
    "autoload": {
        "psr-4": {
            "MyCompany\\Discount\\": "src/"
        }
    },
    "require": {
        "php": ">=7.4"
    }
}
Пример
<?php
namespace MyCompany\Discount;

class DiscountCalculator
{
    public function calculate(float $price, float $percent): float
    {
        if ($percent < 0 || $percent > 100) {
            throw new \InvalidArgumentException('Процент скидки должен быть от 0 до 100');
        }
        return $price - ($price * $percent / 100);
    }
}

Использование в любом PHP проекте (после composer require):

Пример
<?php
require_once 'vendor/autoload.php';

use MyCompany\Discount\DiscountCalculator;

$calc = new DiscountCalculator();
echo $calc->calculate(1000, 10); // 900

Результат:

900

Проблема: Ошибка автозагрузки классов, если не выполнить composer dump-autoload. Решение: запустить composer dump-autoload или composer install.

Пример 3: Laravel компонент с командами Artisan

Создадим пакет, который добавляет команду для очистки логов. Service Provider:

Пример
<?php
namespace App\Providers\LogCleaner;

use Illuminate\Support\ServiceProvider;
use App\Console\Commands\CleanLogs;

class LogCleanerServiceProvider extends ServiceProvider
{
    public function boot()
    {
        if ($this->app->runningInConsole()) {
            $this->commands([
                CleanLogs::class,
            ]);
        }
        $this->publishes([
            __DIR__.'/config/logcleaner.php' => config_path('logcleaner.php'),
        ]);
    }

    public function register()
    {
        $this->mergeConfigFrom(__DIR__.'/config/logcleaner.php', 'logcleaner');
    }
}

Команда:

Пример
<?php
namespace App\Console\Commands;

use Illuminate\Console\Command;

class CleanLogs extends Command
{
    protected $signature = 'logs:clean {--days=7}';
    protected $description = 'Очищает логи старше указанного числа дней';

    public function handle()
    {
        $days = (int)$this->option('days');
        $path = storage_path('logs');
        $files = glob($path.'/*.log');
        $now = time();
        foreach ($files as $file) {
            if ($now - filemtime($file) > $days * 86400) {
                unlink($file);
                $this->info("Удалён: $file");
            }
        }
        $this->info('Очистка завершена.');
    }
}

Выполнение php artisan logs:clean --days=14 - удалит файлы старше 14 дней. Вывод:

Удалён: /var/www/storage/logs/laravel-2025-03-01.log
Очистка завершена.

Проблема: Если путь к логам изменён в конфигурации, файлы не найдутся. Решение: читать путь из config('logging.default') или явно задавать в конфиге пакета.

PHP компоненты - comments

En
Php components (php)