[READ-ONLY] Speed up your package DI containers integration and console apps to Symfony and Nette
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
src
tests
.gitattributes
.gitignore
.travis.yml
LICENSE
README.md
composer.json
phpunit.xml

README.md

Package Builder

Build Status Downloads

This tools helps you with Collectors in DependecyInjection, Console shortcuts, ParameterProvider as service and many more.

Install

composer require symplify/package-builder

Use

Prevent Parameter Typos

Was it ignoreFiles? Or ignored_files? Or ignore_file? Are you lazy to ready every README.md to find out the corrent name? Make developer's live happy by helping them.

parameters:
    correctKey: 'value'

services:
    _defaults:
        public: true
        autowire: true

    Symfony\Component\EventDispatcher\EventDispatcher: ~
    # this subscribe will check parameters on every Console and Kernel run
    Symplify\PackageBuilder\EventSubscriber\ParameterTypoProofreaderEventSubscriber: ~

    Symplify\PackageBuilder\Parameter\ParameterTypoProofreader:
        $correctToTypos:
            # correct key name
            correct_key:
                # the most common typos that people make
                - 'correctKey'

                # regexp also works!
                - '#correctKey(s)?#i'

This way user is informed on every typo he or she makes via exception:

Parameter "parameters > correctKey" does not exist.
Use "parameters > correct_key" instead.

They can focus less on remembering all the keys and more on programming.


Collect Services of Certain Type Together

How do we load Commands to Console Application without tagging?

<?php declare(strict_types=1);

namespace App\DependencyInjection\CompilerPass;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symplify\PackageBuilder\DependencyInjection\DefinitionFinder;
use Symplify\PackageBuilder\DependencyInjection\DefinitionCollector;

final class CollectorCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $containerBuilder): void
    {
        $definitionCollector = new DefinitionCollector(new DefinitionFinder());

        $definitionCollector->loadCollectorWithType(
            $containerBuilder,
            Application::class, // 1 main service
            Command::class, // many collected services
            'add' // the adder method called on 1 main service
        );
    }
}

Add Service by Interface if Found

<?php declare(strict_types=1);

namespace App\DependencyInjection\CompilerPass;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symplify\EasyCodingStandard\Contract\Finder\CustomSourceProviderInterface;
use Symplify\EasyCodingStandard\Finder\SourceFinder;
use Symplify\PackageBuilder\DependencyInjection\DefinitionFinder;

final class CustomSourceProviderDefinitionCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $containerBuilder): void
    {
        $definitionFinder = new DefinitionFinder();

        $customSourceProviderDefinition = $definitionFinder->getByTypeIfExists(
            $containerBuilder,
            CustomSourceProviderInterface::class
        );

        if ($customSourceProviderDefinition === null) {
            return;
        }

        $sourceFinderDefinition = $definitionFinder->getByType($containerBuilder, SourceFinder::class);
        $sourceFinderDefinition->addMethodCall(
            'setCustomSourceProvider',
            [new Reference($customSourceProviderDefinition->getClass())]
        );
    }
}

Get All Parameters via Service

# app/config/services.yml

parameters:
    source: "src"

services:
    _defaults:
        autowire: true

    Symplify\PackageBuilder\Parameter\ParameterProvider: ~

Then require in __construct() where needed:

<?php declare(strict_types=1);

namespace App\Configuration;

use Symplify\PackageBuilder\Parameter\ParameterProvider;

final class StatieConfiguration
{
    /**
     * @var ParameterProvider
     */
    private $parameterProvider;

    public function __construct(ParameterProvider $parameterProvider)
    {
        $this->parameterProvider = $parameterProvider;
    }

    public function getSource(): string
    {
        return $this->parameterProvider->provideParameter('source'); // returns "src"
    }
}

Get Vendor Directory from Anywhere

<?php declare(strict_types=1);

Symplify\PackageBuilder\Composer\VendorDirProvider::provide(); // returns path to vendor directory

Load a Config for CLI Application?

Use in CLI entry file bin/<app-name>, e.g. bin/statie or bin/apigen.

<?php declare(strict_types=1);

# bin/statie

use Symfony\Component\Console\Input\ArgvInput;
use Symplify\PackageBuilder\Configuration\ConfigFileFinder;

ConfigFileFinder::detectFromInput('statie', new ArgvInput);
# throws "Symplify\PackageBuilder\Exception\Configuration\FileNotFoundException" exception if no file is found

Where "statie" is key to save the location under. Later you'll use it get the config.

With --config you can set config via CLI.

bin/statie --config config/statie.yml

Then get the config just run:

<?php declare(strict_types=1);

$config = Symplify\PackageBuilder\Configuration\ConfigFileFinder::provide('statie');
var_dump($config); // returns absolute path to "config/statie.yml"
// or NULL if none was found before

You can also provide fallback to file in current working directory:

<?php declare(strict_types=1);

$config = Symplify\PackageBuilder\Configuration\ConfigFileFinder::provide('statie', ['statie.yml']);
$config = Symplify\PackageBuilder\Configuration\ConfigFileFinder::provide('statie', ['statie.yml', 'statie.yaml']);

This is common practise in CLI applications, e.g. PHPUnit looks for phpunit.xml.


Render Fancy CLI Exception Anywhere You Need

Do you get exception before getting into Symfony\Console Application, but still want to render it with -v, -vv, -vvv options?

<?php declare(strict_types=1);

use Symfony\Component\Console\Application;
use Symfony\Component\DependencyInjection\Container;
use Symplify\PackageBuilder\Console\ThrowableRenderer;

require_once __DIR__ . '/autoload.php';

try {
    /** @var Container $container */
    $container = require __DIR__ . '/container.php';

    $application = $container->get(Application::class);
    exit($application->run());
} catch (Throwable $throwable) {
    (new ThrowableRenderer())->render($throwable);
    exit($throwable->getCode());
}

Load Config via --level Option in CLI App

In you bin/your-app you can use --level option as shortcut to load config from /config directory.

It makes is easier to load config over traditional super long way:

vendor/bin/your-app --config vendor/organization-name/package-name/config/subdirectory/the-config.yml
<?php declare(strict_types=1);

use App\DependencyInjection\ContainerFactory;
use Symfony\Component\Console\Input\ArgvInput;
use Symplify\PackageBuilder\Configuration\ConfigFileFinder;
use Symplify\PackageBuilder\Configuration\LevelFileFinder;

// 1. Try --level
$configFile = (new LevelFileFinder)->detectFromInputAndDirectory(new ArgvInput, __DIR__ . '/../config/');

// 2. try --config
if ($configFile === null) {
    ConfigFileFinder::detectFromInput('ecs', new ArgvInput);
    $configFile = ConfigFileFinder::provide('ecs', ['easy-coding-standard.yml']);
}

// 3. Build DI container
$containerFactory = new ContainerFactory; // your own class
if ($configFile) {
    $container = $containerFactory->createWithConfig($configFile);
} else {
    $container = $containerFactory->create();
}

And use like this:

vendor/bin/your-app --level the-config

Merge Parameters in .yaml Files Instead of Override?

In Symfony the last parameter wins by default*, which is bad if you want to decouple your parameters.

# first.yml
parameters:
    another_key:
       - skip_this
# second.yml
imports:
    - { resource: 'first.yml' }

parameters:
    another_key:
       - skip_that_too

The result will change with Symplify\PackageBuilder\Yaml\FileLoader\ParameterMergingYamlFileLoader:

 parameters:
     another_key:
+       - skip_this
        - skip_that_too

How to use it?

<?php declare(strict_types=1);

namespace App;

use Symfony\Component\Config\Loader\DelegatingLoader;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Loader\GlobFileLoader;
use Symfony\Component\HttpKernel\Config\FileLocator;
use Symfony\Component\HttpKernel\Kernel;
use Symplify\PackageBuilder\Yaml\FileLoader\ParameterMergingYamlFileLoader;

final class AppKernel extends Kernel
{
    /**
     * @param ContainerInterface|ContainerBuilder $container
     */
    protected function getContainerLoader(ContainerInterface $container): DelegatingLoader
    {
        $kernelFileLocator = new FileLocator($this);

        $loaderResolver = new LoaderResolver([
            new GlobFileLoader($container, $kernelFileLocator),
            new ParameterMergingYamlFileLoader($container, $kernelFileLocator)
        ]);

        return new DelegatingLoader($loaderResolver);
    }
}

In case you need to do more work in YamlFileLoader, just extend the abstract parent Symplify\PackageBuilder\Yaml\FileLoader\AbstractParameterMergingYamlFileLoader and add your own logic.


Do you Need to Merge YAML files Outside Kernel?

Instead of creating all the classes use this helper class:

<?php declare(strict_types=1);

$parameterMergingYamlLoader = new Symplify\PackageBuilder\Yaml\ParameterMergingYamlLoader;

$parameterBag = $parameterMergingYamlLoader->loadParameterBagFromFile(__DIR__ . '/config.yml');

var_dump($parameterBag);
// instance of "Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface"

Use %vendor% and %cwd% in Imports Paths

Instead of 2 paths with ignore_errors use %vendor% and other parameters in imports paths:

 imports:
-    - { resource: '../../easy-coding-standard/config/psr2.yml', ignore_errors: true }
-    - { resource: 'vendor/symplify/easy-coding-standard/config/psr2.yml', ignore_errors: true }
+    - { resource: '%vendor%/symplify/easy-coding-standard/config/psr2.yml' }

You can have that with Symplify\PackageBuilder\Yaml\FileLoader\ParameterImportsYamlFileLoader:

<?php declare(strict_types=1);

namespace App;

use Symfony\Component\Config\Loader\DelegatingLoader;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Loader\GlobFileLoader;
use Symfony\Component\HttpKernel\Config\FileLocator;
use Symfony\Component\HttpKernel\Kernel;
use Symplify\PackageBuilder\Yaml\FileLoader\ParameterImportsYamlFileLoader;

final class AppKernel extends Kernel
{
    /**
     * @param ContainerInterface|ContainerBuilder $container
     */
    protected function getContainerLoader(ContainerInterface $container): DelegatingLoader
    {
        $kernelFileLocator = new FileLocator($this);

        $loaderResolver = new LoaderResolver([
            new GlobFileLoader($container, $kernelFileLocator),
            new ParameterImportsYamlFileLoader($container, $kernelFileLocator)
        ]);

        return new DelegatingLoader($loaderResolver);
    }
}

In case you need to do more work in YamlFileLoader, just extend the abstract parent Symplify\PackageBuilder\Yaml\FileLoader\AbstractParameterImportsYamlFileLoader and add your own logic.


Smart Compiler Passes for Lazy Programmers ↓

How to add compiler pass?


Collect Services in Short Configs

  • Symplify\PackageBuilder\DependencyInjection\CompilerPass\ConfigurableCollectorCompilerPass

Must-have for PHP CLI Apps without FrameworkBundle. Collect services by type, e.g. Console Commands to Application, EventSubscribers to EventDispatcher.

parameters:
    collectors:
        -
            main_type: 'Symplify\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory'
            collected_type: 'Symplify\BetterPhpDocParser\Contract\PhpDocInfoDecoratorInterface'
            add_method: 'addPhpDocInfoDecorator'

Read more about collector pattern to know how and when to use it.


Autowire Singly-Implemented Interfaces

  • Symplify\PackageBuilder\DependencyInjection\CompilerPass\AutowireSinglyImplementedCompilerPass
 services:
     OnlyImplementationOfFooInterface: ~
-
-    FooInterface:
-        alias: OnlyImplementationOfFooInterface

Do not Repeat Simple Factories

  • Symplify\PackageBuilder\DependencyInjection\CompilerPass\AutoReturnFactoryCompilerPass

This prevent repeating factory definitions for obvious 1-instance factories:

 services:
     Symplify\PackageBuilder\Console\Style\SymfonyStyleFactory: ~
     Symfony\Component\Console\Style\SymfonyStyle:
         factory: ['@Symplify\PackageBuilder\Console\Style\SymfonyStyleFactory', 'create']

How this works?

The factory class needs to have return type + create() method:

<?php

namespace Symplify\PackageBuilder\Console\Style;

use Symfony\Component\Console\Style\SymfonyStyle;

final class SymfonyStyleFactory
{
    public function create(): SymfonyStyle
    {
        // ...
    }
}

That's all! The "factory" definition is generated from this obvious usage.

Put this compiler pass first, as it creates new definitions that other compiler passes might work with.

Autowire Array Parameters

  • Symplify\PackageBuilder\DependencyInjection\CompilerPass\AutowireArrayParameterCompilerPass

This feature surpasses YAML-defined, tag-based or CompilerPass-based collectors in minimalistic way:

<?php

class Application
{
    /**
     * @var Command[]
     */
    private $commands = [];

    /**
     * @param Command[] $commands
     */
    public function __construct(array $commands)
    {
        $this->commands = $commands;
        var_dump($commands); // instnace of Command collected from all services
    }
}

Autobind Parameters

  • Symplify\PackageBuilder\DependencyInjection\CompilerPass\AutoBindParametersCompilerPass
 parameters:
     entity_repository_class: 'Doctrine\ORM\EntityRepository'
     entity_manager_class: 'Doctrine\ORM\EntityManager'

 services:
-    _defaults:
-        bind:
-            $entityRepositoryClass: '%entity_repository_class%'
-            $entityManagerClass: '%entity_manager_class%'
-
     Rector\:
         resource: ..

Always Autowire this Type

Do you want to allow users to register services without worrying about autowiring? After all, they might forget it and that would break their code. Set types to always autowire:

<?php

// ...

use PhpCsFixer\Fixer\FixerInterface;
use Symplify\PackageBuilder\DependencyInjection\CompilerPass\AutowireInterfacesCompilerPass;

// ...

        $containerBuilder->addCompilerPass(new AutowireInterfacesCompilerPass([
            FixerInterface::class,
        ]));

This will make sure, that PhpCsFixer\Fixer\FixerInterface is always registered.


Use Public Services only in Tests

 # some config for tests
 services:
-    _defaults:
-        public: true

That's all :)