Skip to content

Custom type

fperugini-b2p edited this page Nov 15, 2022 · 2 revisions

Declare a custom type

This page shows how to create and declare or override a type on Prime.

Overview

Prime types are used at ORM level to parse and normalize entity fields values using methods :

Two levels of types are available :

  • Facade types, implementing FacadeTypeInterface, which performs extra transformation, and forwards storage to an actual database type. This is the base type used to declare a custom type. For example, types json, simpleArray are facade types because they forward to platform types, text in this example.
  • Platform types, implementing PlatformTypeInterface, which are low level types directly handled by the platform. This represents types like int, string...

Types are resolved using their registered name. If a type is natively handled by the platform, the platform type will be used in place of facade type. So types like array or DateTime can be emulated by facade unless platform supports it at low level.

Note: At DBAL level (or if there is no mapping associated to requested column), a type resolution using PHP value is available, by calling PlatformTypesInterface::resolve().

Create a facade type

To create a facade type, you need to extend class Bdf\Prime\Types\AbstractFacadeType.

Then:

  • Call parent constructor with type name (you can use static::class to use type class name as type name).
  • Implements fromDatabase() and toDatabase() to normalize and parse database value.
  • Implements phpType() that defines the PHP entity property type (used by entity generator). It can be a PHP type name or a class name.
  • Implements defaultType() that defines the fallback platform type name.

Example:

Create a type that stores a configuration class into a JSON string:

<?php

use Bdf\Prime\Types\AbstractFacadeType;

class MyOptions
{
    private array $config;
    
    public function __construct(array $config = []) 
    {
        $this->config = $config;
    }
    
    public function foo(): ?string
    {
        return $this->config['foo'] ?? null;
    }
    
    public function toArray(): array
    {
        return $this->config;
    }
}

class MyOptionType extends AbstractFacadeType
{
    public function __construct() 
    {
        parent::__construct(MyOptions::class);
    }

    // Normalize object to an ini string
    public function toDatabase($value)
    {
        // Ignore invalid values
        if (!$value instanceof MyOptions) {
            return null;
        }

        $ini = '';

        foreach ($value->toArray() as $k => $v) {
            $ini .= $k . ' = "' . $v . '"' . PHP_EOL;
        }

        return $ini;
    }

    // Parse database value to domain object
    public function fromDatabase($value, array $fieldOptions = []): ?MyOptions
    {
        // Database value can be null, so return null if data is empty
        if (!$value) {
            return null;
        }

        return new MyOptions(parse_ini_string($value));
    }
    
    public function phpType() : string
    {
        return MyOptions::class;
    }
    
    protected function defaultType()
    {
        // ini config should be saved as human-readable text 
        return self::TEXT;
    }
}

Register the type:

<?php
use Bdf\Prime\ConnectionManager;
use Bdf\Prime\Locatorizable;
use Bdf\Prime\ServiceLocator;
use Bdf\Prime\Connection\ConnectionRegistry;
use Bdf\Prime\Connection\Configuration\ConfigurationResolver;
use Bdf\Prime\Configuration;

// declare your connection manager with a default configuration
$connections = new ConnectionManager(
    new ConnectionRegistry(configResolver: new ConfigurationResolver(default: $config = new Configuration()))
);
$connections->declareConnection('myDB', 'mysql://root@127.0.0.1/test');

// Now you can declare the facade type, which will be available on all connections
$config->getTypes()->register(new MyOptionType());

// Create and configure prime ORM
$prime = new ServiceLocator($connections);
$prime->setDI($kernel->getContainer());
$prime->setSerializer($kernel->getContainer()->get('serializer'));
Locatorizable::configure($prime);

Use custom type on a field:

<?php

use Bdf\Prime\Mapper\Mapper;
use Bdf\Prime\Mapper\Builder\FieldBuilder;

class UserMapper extends Mapper
{
    // ...
    
    /**
     * {@inheritdoc}
     */
    public function buildFields(FieldBuilder $builder): void
    {
        // To use custom type, simply call add() with custom type name as 2nd parameter
        $builder->add('options', MyOptions::class)->nillable();
        //...
    }
    
    // ...
}

Override a platform type

Warning: It's very discouraged to override a platform type. It may cause undefined behaviors !

To create a platform type, extend Bdf\Prime\Platform\AbstractPlatformType and implement missing methods. Constructor should be unmodified : type name will be injected at constructor.

Example:

Type that compresses blob data.

Creation:

<?php

use Bdf\Prime\Platform\AbstractPlatformType;
use Bdf\Prime\Types\PhpTypeInterface;
use Bdf\Prime\Schema\ColumnInterface;
use Doctrine\DBAL\Types\Types;

class CompressedBlob extends AbstractPlatformType
{
    public function toDatabase($value)
    {
        // Compress string to database
        return $value ? gzcompress($value) : $value;
    }
    
    public function fromDatabase($value, array $fieldOptions = [])
    {
        // Uncompress from database
        return $value ? gzuncompress($value) : $value;
    }
    
    public function phpType() : string
    {
        return PhpTypeInterface::STRING;
    }
    
    public function declaration(ColumnInterface $column)
    {
        // Use doctrine type name
        return Types::BLOB;
    }
}

Register the type:

<?php
use Bdf\Prime\ConnectionManager;
use Bdf\Prime\Locatorizable;
use Bdf\Prime\ServiceLocator;
use Bdf\Prime\Connection\ConnectionRegistry;
use Bdf\Prime\Connection\Configuration\ConfigurationResolver;
use Bdf\Prime\Configuration;
use Bdf\Prime\Types\TypeInterface;

// declare your connection manager with a default configuration
$connections = new ConnectionManager();
$connections->declareConnection('myDB', 'mysql://root@127.0.0.1/test');

// Declare platform type per database connection
$connections->getConnection('myDB')->getConfiguration()->addPlatformType(CompressedBlob::class, TypeInterface::BLOB); // Override "blob" type

// Create and configure prime ORM
$prime = new ServiceLocator($connections);
$prime->setDI($kernel->getContainer());
$prime->setSerializer($kernel->getContainer()->get('serializer'));
Locatorizable::configure($prime);

The overridden type is now used natively:

<?php

use Bdf\Prime\Mapper\Mapper;
use Bdf\Prime\Mapper\Builder\FieldBuilder;

class AvatarMapper extends Mapper
{
    // ...
    
    /**
     * {@inheritdoc}
     */
    public function buildFields(FieldBuilder $builder): void
    {
        // CompressedBlob is used here instead of native blob
        $builder->blob('imageData');
        //...
    }

    // ...
}

Clone this wiki locally