How to reuse code with symfony 5 bundles? Part 4. Host Bundle Extension

Let's talk about how to stop copy-paste between projects and transfer the code to a re-usable symfony 5 plug-in bundle. A series of articles summarizing my experience with bundles will lead in practice from creating a minimal bundle and refactoring a demo application to tests and the bundle release cycle.


When designing a bundle, you need to think about what should be encapsulated inside it, and what should be accessible to the user. Should a bundle have fixed functionality or should it be flexible and allow itself to expand? If flexibility is required, then you need to provide some integration points for expanding the bundle, its interface.


Let's try to provide such points in our demo application. In this article:


  • Connecting custom logic to a bundle
  • Tagging
  • Compiler pass
  • Auto Configure Services


If you do not consistently complete the tutorial, then download the application from the repository and switch to the 3-integration branch .


Instructions for installing and starting the project in a file README.md.
You will find the final version of the code for this article in the 4-extend branch .


Task


The calendar has the function of exporting events to GoogleCalendar or iCalendar.
Our task is to make our bundle more flexible and add the ability for bundle users to expand it with their own export formats in their applications.


For example, add the export to a JSON file. Let's get started.



How is the export of events organized?


To understand how to add a new format, let's see how EventExporter works .


Export logic is implemented in the component EventExporterthat is located at services/EventExporter. We have already moved it to the bundle and corrected the namespace names. The main component files are:


  • ExporterInterface simulating event export format and
  • ExporterManager,

ExporterInterface.


β€” , ( Google Calendar), ( iCalendar). inline. EventController::export(), .


,


  • inline

2 AbtractInlineExporter AbstractFileExporter. ( ), , (ExportedFile).


2 β€” GoogleCalendarExporter ICalendarExporter.


, inline , , .



- JSON-.


:


git checkout 4-extend -- src/Service/EventExporter/JsonExporter.php

src/Service/EventExporter/JsonExporter.php:


<?php
declare(strict_types=1);

namespace App\Service\EventExporter;

use bravik\CalendarBundle\Entity\Event;
use bravik\CalendarBundle\Service\EventExporter\AbstractFileExporter;
use bravik\CalendarBundle\Service\EventExporter\ExportedFile;

/**
 * Generates a JSON file
 */
class JsonExporter extends AbstractFileExporter
{
    private const DATE_FORMAT = 'Y-m-d H:i:s';

    public function getName(): string
    {
        return ' JSON';
    }

    public function getType(): string
    {
        return 'json-file';
    }

    public function export(Event $event): ExportedFile
    {
        $data = [
            'id'            => $event->getId(),
            'title'         => $event->getTitle(),
            'description'   => $event->getDescription(),
            'venueName'     => $event->getVenueName(),
            'venueAddress'  => $event->getVenueAddress(),
            'startsAt'      => $event->getStartsAt()->format(self::DATE_FORMAT),
            'endsAt'        => $event->getEndsAt() ? $event->getEndsAt()->format(self::DATE_FORMAT) : null,
        ];

        return new ExportedFile('event.json', 'application/json', json_encode($data));
    }
}

?


DI- config/services.yaml . ExporterManager .


Symfony - . , , ExporterManager services.yaml . :


bravik\CalendarBundle\Service\EventExporter\ExporterManager:
    arguments:
        $exporters:
            - '@bravik\CalendarBundle\Service\EventExporter\Exporters\GoogleCalendarExporter'
            - '@bravik\CalendarBundle\Service\EventExporter\Exporters\ICalendarExporter'
            - 'App\Service\EventExporter\Exporters\JsonExporter'

: , , , .


, - , , ExporterManager - ? .


, . .

ExporterManager.


, , ExporterManager::registerExporter(). . , - registerExporter. DI-.



Symfony . β€” , DI- - . , ExporterManager.


JsonExporter services.yaml -:


App\Service\EventExporter\JsonExporter:
    tags: ['bravik.calendar.exporter']

services.yaml :


bravik\CalendarBundle\Service\EventExporter\Exporters\GoogleCalendarExporter:
    tags: ['bravik.calendar.exporter']
bravik\CalendarBundle\Service\EventExporter\Exporters\ICalendarExporter:
    tags: ['bravik.calendar.exporter']

, , , vendor.package.name.



Symfony 3.4 .


bravik.calendar.exporter ExporterManager. services.yaml:


bravik\CalendarBundle\Service\EventExporter\ExporterManager:
    arguments:
        $exporters: !tagged bravik.calendar.exporter

!tagged <tag-name> iterable , . typehint ExporterManager:


    public function __construct(iterable $exporters) {
    //...
    }

, .


, Β« Β» Β« JSONΒ», JSON-.


( - , 4-extend .)


!tagged <tag-name>, , β€” , . .


Compiler Pass


services.yaml DI- . . Symfony , , PHP-. Symfony .

Compiler Pass. Symfony Compiler Pass.


, , ExporterManager.


bundles/CalendarBundle/src/DependencyInjection/Compiler/ExporterRegistrationPass.php, CompilerPassInterface:


namespace bravik\CalendarBundle\DependencyInjection\Compiler;

use bravik\CalendarBundle\Service\EventExporter\ExporterManager;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class ExporterRegistrationPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (!$container->has(ExporterManager::class)) {
            return;
        }

        $exporterManagerDefinition = $container->findDefinition(ExporterManager::class);

        $taggedServices = $container->findTaggedServiceIds('bravik.calendar.exporter');

        $exporterReferences = [];
        foreach ($taggedServices as $id => $tags) {
            $exporterReferences[] = new Reference($id);
        }

        $exporterManagerDefinition->setArguments(['$exporters' => $exporterReferences]);
    }
}

Compiler Pass src/CalendarBundle. Bundle Bundle::build() :


    public function build(ContainerBuilder $container)
    {
        parent::build($container);
        $container->addCompilerPass(new ExporterRegistrationPass());
    }

, pass , die("pass") process . pass β€” .


?


ExporterManager , DI-.
ExporterManager :


$exporterManagerDefinition = $container->findDefinition(ExporterManager::class);

Definition?


, - (Definition).


DI- , , «». Definition «»: , .

, - . , , , DI- .


, -. bravik.calendar.exporter.


$taggedServices = $container->findTaggedServiceIds('bravik.calendar.exporter');

, Reference , , ExporterManager, $exporters .


$exporterReferences = [];
foreach ($taggedServices as $id => $tags) {
    $exporterReferences[] = new Reference($id);
}

$exporterManagerDefinition->setArguments(['$exporters' => $exporterReferences]);

services.yaml ExporterManager:


bravik\CalendarBundle\Service\EventExporter\ExporterManager: ~

, .


(autoconfiguration)


services.yaml.


, 2 :


bravik\CalendarBundle\Service\EventExporter\Exporters\GoogleCalendarExporter:
    tags: ['bravik.calendar.exporter']
bravik\CalendarBundle\Service\EventExporter\Exporters\ICalendarExporter:
    tags: ['bravik.calendar.exporter']

:


App\Service\EventExporter\JsonExporter:
    tags: ['bravik.calendar.exporter']

, 3, 30 . .


, .

ExporterInterface.


β„–1. _instanceof


services.yaml :


_instanceof:
    # Apply tag to all ExporterInterface implementations
    bravik\CalendarBundle\Service\EventExporter\ExporterInterface:
        tags: ['bravik.calendar.exporter']

β€” !


App\Service\EventExporter\JsonExporter, .
, , . , .


β„–2. Symfony


, DependencyInjection/CalendarExtension load():


 public function load(array $configs, ContainerBuilder $container)
{
    $container->registerForAutoconfiguration(ExporterInterface::class)
        ->addTag('bravik.calendar.exporter');

   //...
}

Symfony , , .


, ExporterManager. :


# Register all EventExporter classes as injectable services
bravik\CalendarBundle\Service\EventExporter\:
    resource: '../src/Service/EventExporter/*'

.



Symfony, , . Symfony: , , Compiler Pass .


4-extend.


, , Symfony .


You can read about some even more advanced examples of redefining services and using service aliases to create extension points:
( https://symfonycasts.com/screencast/symfony-bundle/override-service#play )


Other articles in the series:


Part 1. The minimum bundle
Part 2. We take out the code and templates in the bundle
Part 3. Integration of the bundle with the host: templates, styles, JS
Part 4. Interface for expanding the bundle
Part 5. Parameters and configuration
Part 6. Testing, microapplication inside the bundle
Part 7 Release cycle, installation and update


All Articles