Skip to content

Type

Installation

tbc.

Type Pattern

The Type Pattern is a behavioral pattern which is widely used in enhavo. It is not an official pattern like the ones from Gang of Four, which you probably know. While developing enhavo, we realized that we need an abstract workflow to convert configurations directly into php classes. So over time we found this pattern in our code and standardized it.

For developers who use enhavo as a normal CMS, it is not important to understand this pattern in detail, but the more you work with enhavo, the more interesting it will be for you.

The goal of this pattern is to create objects with an encapsulated configuration and different behaviours - but using the same api.

Think of different actions or buttons which can be configured easily in a yaml file like this:

yaml
create:
    type: create
save:
    type: save
    route: my_save_route
delete:
    type: delete
    label: My custom delete label
    icon: trash

The simplest configuration for a type is just the type option itself. When creating the type we handle the options with the Symfony OptionResolver. It allows you to set the possible options, and also the defaults and required options.

Later on you want a object with the same api to handle your actions, but you don't have to take care about how this object is configured inside.

php
$actions = $manager->getActions($configuration);
$viewData = [];
foreach($actions as $action) {
    $viewData[$action->getKey()] = $action->createViewData($resource);
}

As a user of actions you don't want to deal with OptionResolver, this logic should be encapsulated inside the action. The assembly of the view data is delegated to the type. After you get the actions from the manager, you don't need the configuration anymore.

php
class SaveActionType extends ActionTypeInterface
{
    public function createViewData(array $options, $resource = null)
    {
        return [
            'label' => $options['label'],
            'icon' => $options['icon'],
            'url' => $this->router->generate($options['route']);
        ];
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'label' => 'Save'
            'icon' => 'disk',
        ])
        $resolver->setRequired('route');
    }
}

Thanks to the symfony dependency injection container we can add this type to the central repository by tagging, and it will only be instantiated if it is really used.

The following UML shows the classes involved in the Type Pattern.

image

Define type system

To define a new type system, we need a concrete class, that uses our types, and add type compiler that collects it. Later we add types and use them.

Concrete class and type interface

Let's imagine we want to make a new type system for actions, where we can pass data to the frontend and execute on the server. So we need to define a getViewData and an execute function.

php

namespace App\Action;

class Action extends AbstractTypeContainer
{
    public function getViewData(): array
    {
        
    }
    
    public function execute($resource): void
    {
    
    }
}

When we create a type interface, we may have a hierarchy of types, so instead of return the view data, we let it build.

php

namespace App\Action;

use Enhavo\Component\Type\TypeInterface;

interface ActionTypeInterface extends TypeInterface
{
    public function buildViewData(array $options, &$data): void;

    public function execute(array $options, $resource): void;
}

So we can call our types in the concrete class now.

php

namespace App\Action;

/**
 * @property ActionTypeInterface $type
 * @property ActionTypeInterface $parents
 */
class Action extends AbstractTypeContainer
{
    public function getViewData(): array
    {
        $data = []; 
        foreach ($this->parents as $parent) { 
            $parent->buildViewData($this->options, $data); 
        } 
        $this->type->buildViewData($this->options, $data); 
        return $data; 
    }
    
    public function execute($resource): void
    {
        $this->type->execute($this->options, $resource); 
    }
}

Type compiler

We want to name our type system Action. The factory is very generic, so we add a type compiler that add the factory as a service and auto collects our types with the tag action.

You need to add the type compiler at the kernel or bundle build function.

php
namespace App;

use App\Action\Action;
use Enhavo\Component\Type\TypeCompilerPass;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel
{
    protected function build(ContainerBuilder $container): void
    { 
        parent::build($container); 
        $container->addCompilerPass(new TypeCompilerPass('Action', 'action', Action::class)); 
    } 
}
php
namespace Bundle\MyBundle;

use App\Action\Action;
use Enhavo\Component\Type\TypeCompilerPass;

class MyBundleBundle extends Bundle
{
    public function build(ContainerBuilder $container): void
    { 
        parent::build($container); 
        $container->addCompilerPass(new TypeCompilerPass('Action', 'action', Action::class)); 
    } 
}

If you work with auto wiring you can add the action tag automatically to the interface.

php
public function build(ContainerBuilder $container): void 
{            
    parent::build($container); 
    $container->addCompilerPass(TypeCompilerPass('Action', 'action', Action::class));

    $container 
        ->registerForAutoconfiguration('App\Action\ActionTypeInterface')  
        ->addTag('action');  
}

Add and use type

Add a new type that implement our ActionTypeInterface.

php
namespace App\Action\Type;

use App\Action\ActionTypeInterface;
use Enhavo\Component\Type\AbstractType;
use Doctrine\ORM\EntityManagerInterface;

class SaveActionType extends AbstractType implements ActionTypeInterface
{
    public function __construct(
        private readonly EntityManagerInterface $em,
    )
    {
    }

    public function buildViewData(array $options, &$data): void
    {
        $data['label'] = $options['label'];
        $data['icon'] = $options['icon'];
    }

    public function execute(array $options, $resource): void
    {
        $this->em->persist($resource);
        $this->em->flush();
    }
    
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'label' => 'Save',
            'icon' => 'save',
        ]);
    }

    public static function getName(): ?string
    {
        return 'save';
    }
}

Now you can retrieve an action with a save type.

php
namespace App\Controller;

use Enhavo\Component\Type\FactoryInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

class BookController
{
    public function __construct(
        private readonly FactoryInterface $actionFactory,
    )
    {
    }

    public function index(Request $request)
    {
        $action = $this->actionFactory->create([
            'type' => 'save',
            'icon' => 'disk',
        ])
        
        $data = $action->getViewData();
        
        return new JsonResponse($data);
    }
    
    public function save(Request $request)
    {
        $resource = $this->getResourceByRequest();
    
        $action = $this->actionFactory->create([
            'type' => 'save',
        ])
        
        $action->execute($resource);
        
        return new JsonResponse();
    }
    
    private function getResourceByRequest(Request $request) 
    {
        // ...
    }
}

Abstract and base type

tbc.

Type extension

tbc.