Useful composer packages outside Apie

 


When developing Apie I set up the project in around 30 composer packages. Some of them are in particular for internal usage (for example apie/html-builders or apie/common), but some of them could also be useful in any PHP project.

Remember that since the Apie library has no stable release yet, you might have to require version 1.0.0.x-dev and minimum-stability: dev to make them work with Composer.

Dateformat to regex

PHP has some very confusing date format string format (so to convert a date object into a readable string in a specific format). I've created an article before about creating date value objects. I wanted to make sure the OpenAPI spec will be able to describe the format and the only option is by adding a regular expression. So that's where this library is being used for!
<?php

use Apie\DateformatToRegex\DateFormatToRegex;

$dateFormat = DateTime::ATOM;
$regularExpression = DateFormatToRegex::formatToRegex($dateFormat);
Of course this library gives a simple regular expression, so february the 30th will still be a valid value according this generated regular expression. In the future I might add support to validate this, but this is a much more complex problem than you will initially think about it as I would have to make a SAT solver to figure out all possible combinations possible for a date format string and date and time specifications are quite a mess to get right in a regular expression.

Regex tools

In PHP there is only one string type and you could make string value objects next to it. In the database you have several types of strings. It's not advised to store all strings in the largest possible string column type as they often have a few downsides as well (I ran into issues that a data row was too large for MySQL for example when I made all columns longtext or blob).

For string types I can assume that they always have infinite length, so I have no choice, but for value objects it would be possible to know the maximum string length, for example a hostname can not be longer than 128 characters. I could manually make a static getMaximumStringLength() method to a value object as metadata, but this felt a bit redundant as it could often be deduced in a regular expression. I moved the code to its own package. There is still some regex helper tools in apie/core that I will be moving to this package instead. For example it has a helper method to get rid of delimiters of a regular expression (as OpenAPI spec wants this)

$compiledRegex = \Apie\RegexTools\CompiledRegularExpression::createFromRegexWithoutDelimiters('^test-me$');
$compiledRegex->getMinimalPossibleLength(); // returns 7
$compiledRegex->getMaximumPossibleLength(); // returns 7

In the future I would probably add more regular expression related code, for example I want to generate fake data from a regular expression. There is already a backend package for, but it seems not to be kept up to date as my PR is still open after a few months. Right now the regular expression is created without a parse tree, but with a tokenstream. The result is that it might not work correctly with '|' now as it will parse (abc|def) incorrect. Instead of parsing it as 'abc' or 'def' it will parse it as: 'a', then 'b', then 'c' or 'def'.

Counting words

This is a simple package with only one method. It is being used to index words for a simple TF IDF search algorithm. It counts all the words in a string and returns them in array form:

$counts = \Apie\CountWords\CountWords::countFromString('Ik verdien meer dan ik verdien');
// this results in ['verdien' => 2, 'ik' => 2, 'meer' => 1, 'dan' => 1]
This is all it does! Nothing fancy or whatever.

Service provider generator

My library works in Symfony and Laravel. Both frameworks use a dependency injection service container and a service container has a PSR so multiple libraries can retrieve services in a standard framework way. But when it comes to registering classes for the service container both frameworks have their own implementation and differ greatly. This would imply you have to write service registration code twice.
Since Symfony is more rigid in service registration I made a package that converts a services.yaml file that would be used by a Symfony application and made it create a service provider class for Laravel that registers the exact same services with the same structure.

For example if you have a services.yaml for Symfony like this:
services:
  Apie\Common\Wrappers\ConsoleCommandFactory:
    arguments:
      - '@Apie\Console\ConsoleCommandFactory'
      - '@Apie\Core\ContextBuilders\ContextBuilderFactory'
      - '@Apie\Core\BoundedContext\BoundedContextHashmap'

  apie.console.factory:
    alias: Apie\Common\Wrappers\ConsoleCommandFactory
    public: true

  Apie\Console\ConsoleCommandFactory:
    arguments:
      - '@Apie\Common\ApieFacade'
      - '@Apie\Common\ActionDefinitionProvider'
      - '@Apie\Console\ApieInputHelper'

  Apie\Console\ApieInputHelper:
    factory: ['Apie\Console\ApieInputHelper', 'create']
    arguments:
      - !tagged_iterator Apie\Console\Helpers\InputInteractorInterface
    tags:
      - name: console.helper
This library can output this service provider class for Laravel:
<?php
namespace Apie\Console;

use Apie\ServiceProviderGenerator\UseGeneratedMethods;
use Illuminate\Support\ServiceProvider;

/**
 * This file is generated with apie/service-provider-generator from file: console.yaml
 * @codeCoverageIgnore
 */
class ConsoleServiceProvider extends ServiceProvider
{
    use UseGeneratedMethods;

    public function register()
    {
        $this->app->singleton(
            \Apie\Common\Wrappers\ConsoleCommandFactory::class,
            function ($app) {
                return new \Apie\Common\Wrappers\ConsoleCommandFactory(
                    $app->make(\Apie\Console\ConsoleCommandFactory::class),
                    $app->make(\Apie\Core\ContextBuilders\ContextBuilderFactory::class),
                    $app->make(\Apie\Core\BoundedContext\BoundedContextHashmap::class)
                );
            }
        );
        $this->app->bind('apie.console.factory', \Apie\Common\Wrappers\ConsoleCommandFactory::class);
        
        $this->app->singleton(
            \Apie\Console\ConsoleCommandFactory::class,
            function ($app) {
                return new \Apie\Console\ConsoleCommandFactory(
                    $app->make(\Apie\Common\ApieFacade::class),
                    $app->make(\Apie\Common\ActionDefinitionProvider::class),
                    $app->make(\Apie\Console\ApieInputHelper::class)
                );
            }
        );
        $this->app->singleton(
            \Apie\Console\ApieInputHelper::class,
            function ($app) {
                return \Apie\Console\ApieInputHelper::create(
                    $this->getTaggedServicesIterator(\Apie\Console\Helpers\InputInteractorInterface::class)
                );
                
            }
        );
        \Apie\ServiceProviderGenerator\TagMap::register(
            $this->app,
            \Apie\Console\ApieInputHelper::class,
            array(
              0 =>
              array(
                'name' => 'console.helper',
              ),
            )
        );
        $this->app->tag([\Apie\Console\ApieInputHelper::class], 'console.helper');
        
    }
}
The special TagMap class is used to work around a few missing features with tagged services in the Laravel container. For example you can not get the service id's from tagged services and you can not change the order in which they appear. As Symfony maps all services as singleton, the generated service provider will also register them all as singletons in a Laravel application.

phpunit-matrix-data-provider

This package name is a mouthful, but I could not come up with a better name. I've shown this library in a previous (rather inpopular) blog article. This is a helper class to make dataproviders for phpunit tests by using dependency injection and trying all possible combinations. For example let's imagine I have these classes and want to test all combinations possible with dependency injection:

class SomeService {
    public function __construct(DatabaseConnection $databaseConnection) {
    }
}
class SomeOtherService {
    public function __construct(DatabaseConnection $databaseConnection) {
    }
}
class SqliteConnection implements DatabaseConnection {}
class MysqlConnection implements DatabaseConnection {}
class PostgresConnection implements DatabaseConnection {}
class InMemoryFakeConnection implements DatabaseConnection {}
Then we make one helper class object:

class UnittestHelper {
    public function createSomeService(DatabaseConnection $databaseConnection): SomeService {
    	return new SomeService($databaseConnection);
    }
    public function createSomeOtherService(DatabaseConnection $databaseConnection): SomeOtherService {
        return new SomeOtherService($databaseConnection);
    }
    public function createSqliteConnection(): DatabaseConnection
    {
        return new SqliteConnection();
    }
    
    public function createMysqlConnection(): DatabaseConnection
    {
    	return new MysqlConnection();
    }
}
Now in the test I can generate a data provider to give me all possible combinations:

use Apie\PhpunitMatrixDataProvider\MakeDataProviderMatrix;
use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
    public function provideInput()
    {
        yield from $this->createDataProviderFrom(
            new \ReflectionMethod($this, 'testSomething'),
            new UnittestHelper()
        );
    }

    /**
     * @dataProvider provideInput
     */
    public function testSomething(SomeService $someService, SomeOtherService) {
        // the test
    }
}
This test will combine all options of SomeService, SomeOtherService using the create functions in UnittestHelper class. If we would add a new createPostgresConnection() and createInMemoryFakeConnection() we would get 4 new test cases for our testSomething method. I know it looks like a very specific use case it solves, but all apie integration tests are using it internally to test functionality in different frameworks with different configurations.

Comments