Разработка компонентов PHP: от инфоблоков до пакетов Composer
Компонент 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.phpPhp 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') или явно задавать в конфиге пакета.