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:
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.
$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.
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
.
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.
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.
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.
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.
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));
}
}
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.
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
.
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.
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.