Коротко: свой бандл в Symfony — переиспользуемый модуль со своими сервисами, конфигом и DI-расширением; разбираем создание с нуля и регистрацию.

Собственный бандл в Symfony — это переиспользуемый модуль со своими сервисами, конфигурацией и DI-расширением, который можно подключать в разные проекты. Разберём по шагам, как создать свой бандл с нуля и правильно его зарегистрировать.

Что такое бандл в Symfony?

Symfony 6, как и все предыдущие ее версии, имеет систему бандлов. Это такие пакеты / библиотеки, разработанные специально для Symfony фреймворка, чтобы можно было легко подключать тот или иной функционал без проблем. По поводу бандлов есть много разногласий. Кто-то считает их злом, а кто-то добром, но факт в том, что они существуют и используются. Сегодня мы разберем как же создать свой собственный простенький бандл.

Создаём структуру бандла

Создаем директорию lib/PassGeneratorBundle/src
Создаем сервис PassGenerator.php с неймспейсом PassGeneratorBundle;
Создаем тестовый контроллер в основной части проекта например Controller/Test.php

<?php

namespace App\Controller\Tools;

use App\Service\ToolsService;
use PassGeneratorBundle\PassGenerator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

#[Route('/test')]
class Test extends AbstractController
{
    #[Route('/1', name: 'test_1', methods: ['GET'])]
    public function test1(PassGenerator $generator): Response
    {
        return $this->json([
            'pass' => $generator->generatePassword()
        ]);
    }
}


В тестовом контроллере создаем метод, который будет дергать что-то из бандла.

В PassGenerator пишем какую-нибудь логику по генерации паролей.
 

<?php

namespace PassGeneratorBundle;

class PassGenerator
{
    public function generatePassword(): string
    {
        return $this->generatePasswordReal();
    }

    /**
     * @param bool $numbers
     * @param bool $upperCase
     * @param bool $lowerCase
     * @param bool $specialChars
     * @param int $length
     * @return string
     */
    private function generatePasswordReal(
        bool $numbers = true,
        bool $upperCase = true,
        bool $lowerCase = true,
        bool $specialChars = true,
        int $length = 10
    ): string {
        $password = '';

        $symbolsAvailable = [];
        $numbersArr = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
        $upperCaseArr = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
        $lowerCaseArr = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
        $specialCharsArr = ['!', '#', '$', '%', '&', '(', ')', '*', '+', '.', '/', ':', ';', '=', '>', '?', '@', '[', ']', '^', '`', '{', '|', '}', '~'];
        if ($numbers) {
            $symbolsAvailable = array_merge($symbolsAvailable, $numbersArr);
        }
        if ($upperCase) {
            $symbolsAvailable = array_merge($symbolsAvailable, $upperCaseArr);
        }
        if ($lowerCase) {
            $symbolsAvailable = array_merge($symbolsAvailable, $lowerCaseArr);
        }
        if ($specialChars) {
            $symbolsAvailable = array_merge($symbolsAvailable, $specialCharsArr);
        }

        for ($i = 0; $i < $length; $i++) {
            $password .= $symbolsAvailable[mt_rand(0, count($symbolsAvailable) - 1)];
        }

        return $password;
    }
}

Далее, чтобы подцепился новый неймспейс - нужно в composer.json основного проекта добавить в директиву autoload наш новый неймспейс, чтобы можно было бы с ним работать локально (для начала).

"autoload": {
    "psr-4": {
        "PassGeneratorBundle\\": "lib/PassGeneratorBundle/src/",
        "App\\": "src/"
    }
},

После этого нам необходимо запустить команду composer dump-autoload, чтобы неймспейсы подцепились.

Если сейчас запустить контроллер - то мы увидим ошибку с текстом, о том, что мы не можем подцепить сервис - это происходит потому, что Symfony пока что не знает ничего про новый бандл и никак его предварительно не зарегистрировала. Чтобы это быстро пофиксить - мы добавим в файл services.yaml основного проекта строку, которая будет явно объявлять новый сервис.

parameters:

services:
    _defaults:
        autowire: true
        autoconfigure: true

    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'

    PassGeneratorBundle\PassGenerator: ~

Теперь мы можем открыть страницу /test/1 нашего проекта и там мы увидим как уже генерируются пароли. Но пока что это еще не бандл.

Регистрируем бандл в Symfony

Теперь нам необходимо в корне нашего бандла ( lib/PassGeneratorBundle/src/ ) создать класс PassGeneratorBundle.php со следующим содержимым:

<?php

namespace PassGeneratorBundle;

use Symfony\Component\HttpKernel\Bundle\AbstractBundle;

class PassGeneratorBundle extends AbstractBundle
{

}

И теперь мы можем зарегистрировать наш бандл как бандл в Symfony файле config/bundles.php добавив его по аналогии на последнюю строчку:

<?php

return [
    Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],

    //...

    PassGeneratorBundle\PassGeneratorBundle::class => ['all' => true],
];

DI-расширение бандла

Далее нам необходимо создать файл расширения DependencyInjection/PassGeneratorExtension.php

<?php

namespace PassGeneratorBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;

class PassGeneratorExtension extends Extension
{
    /**
     * @param array $configs
     * @param ContainerBuilder $container
     * @return void
     */
    public function load(array $configs, ContainerBuilder $container): void
    {

    }
}

А в PassGeneratorBundle.php добавить метод:

<?php

namespace PassGeneratorBundle;

use PassGeneratorBundle\DependencyInjection\PassGeneratorExtension;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;

class PassGeneratorBundle extends AbstractBundle
{
    public function getContainerExtension(): ?ExtensionInterface
    {
        return new PassGeneratorExtension();
    }
}

Теперь мы постепенно подходим к тому, что наш бандл уже является бандлом и мы хотим сделать его конфигурируемым.

Сервисы бандла (services.yaml)

Теперь нам необходимо создать файл config/services.yaml в корневой директории бандла со следующим содержимым:

parameters:

services:
    _defaults:
        autowire: true
        public: false
        autoconfigure: true

    PassGeneratorBundle\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/PassGeneratorBundle.php'

    PassGeneratorBundle\PassGenerator: ~

А из services.yaml основного проекта удалить строку, которая явно объявляла сервис PassGeneratorBundle\PassGenerator: ~

Теперь, чтобы у нас подцеплялся наш новый конфигурационный файл - нам нужно в Extension файле расписать метод load следующим образом:

<?php

namespace PassGeneratorBundle\DependencyInjection;

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

class PassGeneratorExtension extends Extension
{
    /**
     * @param array $configs
     * @param ContainerBuilder $container
     * @return void
     * @throws Exception
     */
    public function load(array $configs, ContainerBuilder $container): void
    {
        $loader = new YamlFileLoader(
            $container,
            new FileLocator(dirname(__DIR__).'/config')
        );
        $loader->load('services.yaml');
    }
}

После того как мы успешно подключили services.yaml нашего бандла - нам нужно задать нашему основному сервису алиас, чтобы в перспективе мы могли более гибко конфигурировать наш бандл. Для этого мы изменим services.yaml бандла следующим образом:

parameters:

services:
    _defaults:
        autowire: true
        public: false
        autoconfigure: true

    PassGeneratorBundle\:
        resource: '../'
        exclude:
            - '../DependencyInjection/'
            - '../PassGeneratorBundle.php'

    pgb.password_generator:
        class: PassGeneratorBundle\PassGenerator
    PassGeneratorBundle\PassGenerator: '@pgb.password_generator'

Конфигурация бандла

Теперь давайте создадим файл конфигурации нашего бандла в директории проекта по пути config/packages/password_generator.yaml ( не в бандле, а в проекте ).

password_generator:
    passwordLength: 10

И доработаем наш Extension файл добавив метод getAlias в который пропишем название yaml'а

<?php

namespace PassGeneratorBundle\DependencyInjection;

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

class PassGeneratorExtension extends Extension
{
    /**
     * @param array $configs
     * @param ContainerBuilder $container
     * @return void
     * @throws Exception
     */
    public function load(array $configs, ContainerBuilder $container): void
    {
        $loader = new YamlFileLoader(
            $container,
            new FileLocator(dirname(__DIR__).'/config')
        );
        $loader->load('services.yaml');
    }

    public function getAlias(): string
    {
        return 'password_generator';
    }
}

Теперь, чтобы использовать конфигурирование бандла - нам надо определить а какие вообще параметры должны присутствовать у нашего бандла. Делается это с помощью файла DependencyInjection\Configuration.php со следующим содержимым:

<?php
namespace PassGeneratorBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder(): TreeBuilder
    {
        $treeBuilder = new TreeBuilder('password_generator');

        $treeBuilder->getRootNode()
            ->children()
                ->integerNode('passwordLength')->defaultValue(15)->end()
                ->booleanNode('numbers')->defaultTrue()->end()
                ->booleanNode('upperCase')->defaultTrue()->end()
                ->booleanNode('lowerCase')->defaultTrue()->end()
                ->booleanNode('specialChars')->defaultTrue()->end()
            ->end()
        ;

        return $treeBuilder;
    }
}

В этом файле мы определили, что у нас будет несколько наборов конфигов, которые будут коррелировать с нашим файлом config/packages/password_generator.yaml. И давайте его тоже подредактируем, чтобы он полностью отражал все наши возможные настройки.

password_generator:
    passwordLength: 10
    numbers: true
    upperCase: true
    lowerCase: true
    specialChars: false

Теперь, чтобы воспользоваться нашими конфигурационными файлами необходимо сделать доработку в класс расширения в методе load следующим образом:

<?php

namespace PassGeneratorBundle\DependencyInjection;

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

class PassGeneratorExtension extends Extension
{
    /**
     * @param array $configs
     * @param ContainerBuilder $container
     * @return void
     * @throws Exception
     */
    public function load(array $configs, ContainerBuilder $container): void
    {
        $loader = new YamlFileLoader(
            $container,
            new FileLocator(dirname(__DIR__).'/config')
        );
        $loader->load('services.yaml');

        $configuration = $this->getConfiguration($configs, $container);

        $config = $this->processConfiguration($configuration, $configs); //здесь будет лежать наш массив конфигов
    }

    public function getAlias(): string
    {
        return 'password_generator';
    }
}

А это в свою очередь нам даёт возможность передавать эти конфиги внутрь нашего модуля в нужные сервисы следующим образом:

<?php

namespace PassGeneratorBundle\DependencyInjection;

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

class PassGeneratorExtension extends Extension
{
    /**
     * @param array $configs
     * @param ContainerBuilder $container
     * @return void
     * @throws Exception
     */
    public function load(array $configs, ContainerBuilder $container): void
    {
        $loader = new YamlFileLoader(
            $container,
            new FileLocator(dirname(__DIR__).'/config')
        );
        $loader->load('services.yaml');

        $configuration = $this->getConfiguration($configs, $container);

        $config = $this->processConfiguration($configuration, $configs);

        $definition = $container->getDefinition('pgb.password_generator');

        $definition->setArgument('$numbers', $config['numbers']);
        $definition->setArgument('$upperCase', $config['upperCase']);
        $definition->setArgument('$lowerCase', $config['lowerCase']);
        $definition->setArgument('$specialChars', $config['specialChars']);
        $definition->setArgument('$length', $config['passwordLength']);
    }

    public function getAlias(): string
    {
        return 'password_generator';
    }
}
<?php

namespace PassGeneratorBundle;

class PassGenerator
{
    public function __construct(
        private readonly bool $numbers,
        private readonly bool $upperCase,
        private readonly bool $lowerCase,
        private readonly bool $specialChars,
        private readonly int $length,
    ) {
    }

    public function generatePassword(): string
    {
        return $this->generatePasswordReal();
    }

    /**
     * @return string
     */
    private function generatePasswordReal(): string
    {
        $password = '';

        $symbolsAvailable = [];
        $numbersArr = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
        $upperCaseArr = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
        $lowerCaseArr = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
        $specialCharsArr = ['!', '#', '$', '%', '&', '(', ')', '*', '+', '.', '/', ':', ';', '=', '>', '?', '@', '[', ']', '^', '`', '{', '|', '}', '~'];
        if ($this->numbers) {
            $symbolsAvailable = array_merge($symbolsAvailable, $numbersArr);
        }
        if ($this->upperCase) {
            $symbolsAvailable = array_merge($symbolsAvailable, $upperCaseArr);
        }
        if ($this->lowerCase) {
            $symbolsAvailable = array_merge($symbolsAvailable, $lowerCaseArr);
        }
        if ($this->specialChars) {
            $symbolsAvailable = array_merge($symbolsAvailable, $specialCharsArr);
        }

        for ($i = 0; $i < $this->length; $i++) {
            $password .= $symbolsAvailable[mt_rand(0, count($symbolsAvailable) - 1)];
        }

        return $password;
    }
}

Теперь у нас есть возможность переопределять конфигурации бандла внутри проекта пользовательскими ямлами, что делает бандл более расширяемым и конфигурируемым.

Расширяем возможности бандла

Чтобы продемонстрировать чуть больше возможностей внутри самого бандла давайте создадим PassContentInterface, который будет включать в себя методы: getNumbers(), getUpperCases(), getLowerCases(), getSpecialChars(). Каждый метод должен возвращать массив со строковыми символами для составления пароля. Также добавим класс PassContent и пробросим интерфейс в PasswordGenerator следующим образом:

<?php

namespace PassGeneratorBundle;

interface PassContentsInterface
{
    /**
     * @return string[]
     */
    public function getNumbers(): array;

    /**
     * @return string[]
     */
    public function getUpperCases(): array;

    /**
     * @return string[]
     */
    public function getLowerCases(): array;

    /**
     * @return string[]
     */
    public function getSpecialChars(): array;
}
<?php

namespace PassGeneratorBundle;

class PassContents implements PassContentsInterface
{
    /**
     * @return string[]
     */
    public function getNumbers(): array
    {
        return ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
    }

    /**
     * @return string[]
     */
    public function getUpperCases(): array
    {
        return ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
    }

    /**
     * @return string[]
     */
    public function getLowerCases(): array
    {
        return ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
    }

    /**
     * @return string[]
     */
    public function getSpecialChars(): array
    {
        return ['!', '#', '$', '%', '&', '(', ')', '*', '+', '.', '/', ':', ';', '=', '>', '?', '@', '[', ']', '^', '`', '{', '|', '}', '~'];
    }
}
<?php

namespace PassGeneratorBundle;

class PassGenerator
{
    public function __construct(
        private readonly bool $numbers,
        private readonly bool $upperCase,
        private readonly bool $lowerCase,
        private readonly bool $specialChars,
        private readonly int $length,
        private readonly PassContentsInterface $passContents
    ) {
    }

    /**
     * @return string
     */
    public function generatePassword(): string
    {
        $password = '';

        $symbolsAvailable = [];
        if ($this->numbers) $symbolsAvailable = array_merge($symbolsAvailable, $this->passContents->getNumbers());
        if ($this->upperCase) $symbolsAvailable = array_merge($symbolsAvailable, $this->passContents->getUpperCases());
        if ($this->lowerCase) $symbolsAvailable = array_merge($symbolsAvailable, $this->passContents->getLowerCases());
        if ($this->specialChars) $symbolsAvailable = array_merge($symbolsAvailable, $this->passContents->getSpecialChars());

        for ($i = 0; $i < $this->length; $i++) {
            $password .= $symbolsAvailable[mt_rand(0, count($symbolsAvailable) - 1)];
        }

        return $password;
    }
}

А также нам надо будет добавить этот новый сервис реализующий интерфейс в services.yaml нашего бандла:

parameters:

services:
    _defaults:
        autowire: true
        public: false
        autoconfigure: true

    PassGeneratorBundle\:
        resource: '../'
        exclude:
            - '../DependencyInjection/'
            - '../PassGeneratorBundle.php'

    pgb.default_pass_contents:
        class: PassGeneratorBundle\PassContents
    PassGeneratorBundle\PassContents: '@pgb.default_pass_contents'

    pgb.password_generator:
        class: PassGeneratorBundle\PassGenerator
        arguments:
            $passContents: '@pgb.default_pass_contents'
    PassGeneratorBundle\PassGenerator: '@pgb.password_generator'

Теперь мы хотим дать пользователю бандла возможность подставлять свои символы для генерации пароля. И сделать он это может путем реализации собственного PassContentsInterface. Для того, чтобы дать пользователю эту возможность - добавим некоторые конфиги следующим образом:

<?php
namespace PassGeneratorBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder(): TreeBuilder
    {
        $treeBuilder = new TreeBuilder('password_generator');

        $treeBuilder->getRootNode()
            ->children()
                ->integerNode('passwordLength')->defaultValue(15)->end()
                ->scalarNode('passContentsInterface')->defaultNull()->info('PassContentsInterface realisation should be passed here')->end()
                ->booleanNode('numbers')->defaultTrue()->end()
                ->booleanNode('upperCase')->defaultTrue()->end()
                ->booleanNode('lowerCase')->defaultTrue()->end()
                ->booleanNode('specialChars')->defaultTrue()->end()
            ->end()
        ;

        return $treeBuilder;
    }
}

Добавили passContentsInterface в дерево конфигураций.

<?php

namespace PassGeneratorBundle\DependencyInjection;

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

class PassGeneratorExtension extends Extension
{
    /**
     * @param array $configs
     * @param ContainerBuilder $container
     * @return void
     * @throws Exception
     */
    public function load(array $configs, ContainerBuilder $container): void
    {
        $loader = new YamlFileLoader(
            $container,
            new FileLocator(dirname(__DIR__).'/config')
        );
        $loader->load('services.yaml');

        $configuration = $this->getConfiguration($configs, $container);

        $config = $this->processConfiguration($configuration, $configs);

        $definition = $container->getDefinition('pgb.password_generator');

        $definition->setArgument('$numbers', $config['numbers']);
        $definition->setArgument('$upperCase', $config['upperCase']);
        $definition->setArgument('$lowerCase', $config['lowerCase']);
        $definition->setArgument('$specialChars', $config['specialChars']);
        $definition->setArgument('$length', $config['passwordLength']);

        if (!empty($config['passContentsInterface'])) {
            $definition->setArgument('$passContents', new Reference($config['passContentsInterface']));
        }
    }

    public function getAlias(): string
    {
        return 'password_generator';
    }
}

Добавляем в Расширение в метод load проверку наличия конфига passContentesInterface и если он есть - то передаем в пассворд генератор класс определенный пользователем. А вот собственно и сам класс, расположенный src/Service/PassContents.php. Представим, что пользователь у нас захотел очень ограниченные пароли, поэтому реализовал вот такой вот интерфейс:

<?php

namespace App\Service;

use PassGeneratorBundle\PassContentsInterface;

class PassContents implements PassContentsInterface
{
    /**
     * @return string[]
     */
    public function getNumbers(): array
    {
        return ['0', '1'];
    }

    /**
     * @return string[]
     */
    public function getUpperCases(): array
    {
        return ['A', 'Z'];
    }

    /**
     * @return string[]
     */
    public function getLowerCases(): array
    {
        return ['a', 'z'];
    }

    /**
     * @return string[]
     */
    public function getSpecialChars(): array
    {
        return ['@', '&'];
    }
}

И добавляем референс на данный класс в конфиг проекта

password_generator:
    passwordLength: 10
    numbers: true
    upperCase: true
    lowerCase: true
    specialChars: false
    passContentsInterface: App\Service\PassContents

Также возможна еще одна в целом более корректная реализация в методе load внутри проверки без использования Reference класса:

<?php

namespace PassGeneratorBundle\DependencyInjection;

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

class PassGeneratorExtension extends Extension
{
    /**
     * @param array $configs
     * @param ContainerBuilder $container
     * @return void
     * @throws Exception
     */
    public function load(array $configs, ContainerBuilder $container): void
    {
        $loader = new YamlFileLoader(
            $container,
            new FileLocator(dirname(__DIR__).'/config')
        );
        $loader->load('services.yaml');

        $configuration = $this->getConfiguration($configs, $container);

        $config = $this->processConfiguration($configuration, $configs);

        $definition = $container->getDefinition('pgb.password_generator');

        $definition->setArgument('$numbers', $config['numbers']);
        $definition->setArgument('$upperCase', $config['upperCase']);
        $definition->setArgument('$lowerCase', $config['lowerCase']);
        $definition->setArgument('$specialChars', $config['specialChars']);
        $definition->setArgument('$length', $config['passwordLength']);

        if (!empty($config['passContentsInterface'])) {
            $container->setAlias('pgb.default_pass_contents', $config['passContentsInterface']);
        }
    }

    public function getAlias(): string
    {
        return 'password_generator';
    }
}

Здесь мы установили на наш дефолтный алиас - алиас от пользователя внутри конструкции if (!empty($config['passContentsInterface'])) {

Публикация бандла

Теперь наш бандл готов, локально протестирован и готов к загрузке в репозиторий. На данный момент предполагается, что у вас есть навыки работы с git.

Первым делом вам необходимо создать репозиторий и инициализировать его в папке бандла.

cd lib/PassGeneratorBundle/
git init
git remote add origin git@gitflic.ru:username/bundle-name.git
git add .
git commit -m "Initial commit"
git push -u origin master

После того как мы запушили наш бандл в git - нам надо инициализировать композер с помощью команды composer init

Далее визард настройки предложит вам ответить на несколько вопросов и в итоге у вас должен получиться примерно такой вот composer.json файл:

{
    "name": "otezvikentiy/pass-generator-bundle",
    "description": "Symfony password generator bundle",
    "type": "symfony-bundle",
    "license": "MIT",
    "autoload": {
        "psr-4": {
            "PassGeneratorBundle\\": "src/"
        }
    },
    "authors": [
        {
            "name": "l.groshev",
            "email": "OtezVikentiy@gmail.com",
            "homepage": "https://otezvikentiy.tech",
            "role": "Creator"
        }
    ],
    "minimum-stability": "stable",
    "require": {
        "php": "^8.1"
    }
}

Как подключить бандл через Composer?

Теперь рассмотрим как подключить уже собранный бандл в наш проект используя composer основного проекта, а не папочку lib.

Для этого открываем composer.json основного проекта, убираем оттуда автоподгрузку неймспейсов бандла, которую мы делали в самом начале статьи.

    "autoload": {
        "psr-4": {
            "PassGeneratorBundle\\": "lib/PassGeneratorBundle/src/", <-- удалить
            "App\\": "src/"
        }
    },

И выполняем команду composer dump-autoload - после выполнения данной команды в корне основного проекта - наш бандл перестанет работать - это нормально.

Теперь мы можем подключить наш бандл через композер, используя нашу папку lib. Для этого в файл composer.json основного проекта надо дописать перед секцией require следующее:

    "repositories": [
        {
            "type": "path",
            "url": "lib/PassGeneratorBundle"
        }
    ],

далее скопировать название нашего проекта из composer.json бандла из секции name и выполнить:

composer require otezvikentiy/pass-generator-bundle:dev-master

Теперь внутри нашей папки vendor основного проекта мы можем найти директорию otezvikentiy/pass-generator-bundle со всеми файлами нашего бандла. И теперь если мы обновим наш проект и зайдем на урл, который готовили в самом начале для проверки - то по урлу /test/1 - увидим онлайн генерацию паролей.

Как ставить бандл из git-репозитория?

Теперь рассмотрим как подключать из репозитория наш бандл.

Чтобы повторно установить бандл, но уже из репозитория - нам нужно запустить команду

composer remove otezvikentiy/pass-generator-bundle

Эта команда удалит из папки vendor нашу папку с бандлом, но отработает с ошибкой, так как у нас остались конфигурационные файлы в проекте - это нормально, не стоит переживать сейчас об этом.

Теперь, чтобы скачивание нашего бандла шло с репозитория - нам необходимо в composer.json нашего основного проекта в разделе repositories указать вместо того, что мы указывали с type = path следующее:

    "repositories": [
        {
            "type": "vcs",
            "url": "https://gitflic.ru/project/otezvikentiy/pass-generator-bundle.git"
        }
    ],

Снова запускаем команду установки нашей библиотеки

composer require otezvikentiy/pass-generator-bundle:dev-master

И теперь уже у нас устанавливается наш бандл из конкретного git-репозитория. Но этого все ещё не достаточно, ведь у нас осталась одна загвоздка - чтобы бандл устанавливался - нам нужно дополнительно прописывать репозиторий в composer.json, а это не очень удобно.

Публикация на Packagist

После того как мы откатали репозиторий и всё настроили - нам необходимо зайти на сайт packagist.org и там на главной странице будет инструкция, которая гласит, что для того, чтобы залить наш бандл в общий доступ - нам нужно:

1) запустить composer validate - убедиться, что composer.json валиден

2) https://packagist.org/packages/submit - проверить и опубликовать наш репозиторий

После этих действий мы можем спокойно удалять секцию repositories из нашего composer.json основного проекта и вызывать composer require спокойно на любом проекте - с этого момента у нас есть open source бандл для симфони, которым могут пользоваться и другие разработчики.

Теги и версионирование

Также настоятельно рекомендуется тегировать вашу библиотеку-бандл, чтобы с ней было удобнее работать. Делать это можо за счет git tag команды.

Например вы сделали ряд полезных изменений в мастер ветке. Сделали git push. После этого вы можете в composer.json вашей библиотеки проставить version 1.0.3 например и выполнить команду гита:

git tag 1.0.3 && git push --tags

После этого зайти на packagist.org и нажать кнопку update в вашей библиотеке - в этот момент ваш новый тег появится в версиях на packagist и люди смогут использовать конкретную версию вашего бандла / библиотеки.

! ВНИМАНИЕ !

Это минимальная инструкция. Каждая библиотека и бандл должны быть закрыты автотестами, а также должны выполняться рекомендации перечисленные здесь: https://symfony.com/doc/current/bundles/best_practices.html

 

Если нужно вынести общую функциональность в переиспользуемый бандл или навести порядок в структуре проекта — помогу на консультации.