Skip to content

Instantly share code, notes, and snippets.

@zemd
Created April 22, 2015 12:30
Show Gist options
  • Save zemd/1e31675a012598bc24e9 to your computer and use it in GitHub Desktop.
Save zemd/1e31675a012598bc24e9 to your computer and use it in GitHub Desktop.
API DDD prototype
<?php
class AddCommandHandlersPass implements CompilerPassInterface
{
/**
* You can modify the container here before it is dumped to PHP code.
*
* @param ContainerBuilder $container
*
* @api
*/
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('opesho_command_builder')) {
return;
}
$definition = $container->getDefinition('opesho_command_builder');
$taggedServices = $container->findTaggedServiceIds('opesho.api.handler');
foreach ($taggedServices as $id => $attributes) {
$def = $container->getDefinition($id);
$attributes = $attributes[0];
$guesser = isset($attributes['guesser']) ? $attributes['guesser'] : null;
if (!$def->isPublic()) {
throw new InvalidArgumentException(sprintf('The service "%s" must be public as event subscribers are lazy-loaded.', $id));
}
$class = $container->getParameterBag()->resolveValue($def->getClass());
$refClass = new ReflectionClass($class);
$interface = 'Opesho\CommonBundle\Event\CommandHandlerInterface';
if (!$refClass->implementsInterface($interface)) {
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface));
}
$guesserClass = null;
if (isset($guesser)) {
$guesserDef = $container->getDefinition($guesser);
$guesserClass = $container->getParameterBag()->resolveValue($guesserDef->getClass());
$guesserInterface = 'Opesho\CommonBundle\Event\Guesser\CommandGuesserInterface';
if (!(new ReflectionClass($guesserClass))->implementsInterface($guesserInterface)) {
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $attributes['guesser'], $guesserInterface));
}
}
$definition->addMethodCall(
'addHandler',
array($id, $class, $guesser)
);
}
}
}
<?php
/**
* @Annotation
* @Target({"CLASS"})
*/
final class ApiHandler
{
/**
* @var string
* @Enum({"POST", "GET"})
*/
public $method;
/** @var string */
public $description;
/** @var string */
public $command;
/** @var string */
public $endpoint;
/**
* @return string
*/
public function getMethod()
{
return strtoupper($this->method);
}
/**
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* @return string
*/
public function getCommand()
{
return $this->command;
}
/**
* @return string
*/
public function getEndpoint()
{
return $this->endpoint;
}
}
<?php
class CommandBuilder extends ContainerAware
{
/** @var array */
protected $services = array();
/** @var Reader */
protected $reader;
/** @var Instantiator */
protected $instantiator;
/** @var LegacyValidator */
protected $validator;
/** @var EventDispatcherInterface */
protected $dispatcher;
/**
* @param Reader $reader
* @param LegacyValidator $validator
* @param EventDispatcherInterface $dispatcher
*/
public function __construct(Reader $reader, $validator, EventDispatcherInterface $dispatcher)
{
$this->reader = $reader;
$this->validator = $validator;
$this->dispatcher = $dispatcher;
$this->instantiator = new Instantiator();
}
/**
* @param $id
* @param string $handlerClass
* @param string|null $guesser
* @throws \Exception
*/
public function addHandler($id, $handlerClass, $guesser = null)
{
$handlerClass = new ReflectionClass($handlerClass);
/** @var ApiHandler|null $apiHandlerAnnotation */
$apiHandlerAnnotation = $this->reader->getClassAnnotation($handlerClass, ApiHandler::class);
if (is_null($apiHandlerAnnotation)) {
throw new Exception("ApiHandler annotation is missed for handler class.");
}
$this->services[$apiHandlerAnnotation->getEndpoint()] = array(
'handler_service' => $id,
'handler_class' => $handlerClass,
'endpoint' => $apiHandlerAnnotation->getEndpoint(),
'method' => $apiHandlerAnnotation->getMethod(),
'command_class' => $apiHandlerAnnotation->getCommand(),
'guesser_service' => $guesser,
);
}
/**
* @param $service
* @param $method
* @param Request $request
* @return CommandEvent
* @throws \Exception
*/
public function handle($service, $method, Request $request)
{
$endpoint = "{$service}/{$method}";
if (!isset($this->services[$endpoint])) {
throw new Exception("No endpoint handler");
}
$serviceInfo = $this->services[$endpoint];
$command = $this->buildCommand($serviceInfo, $request);
$errors = $this->validator->validate($command, null, false, true);
if ($errors->count() > 0) {
throw new Exception("Command validation error: " . var_export($errors, true));
}
/** @var CommandHandlerInterface $handler */
$handler = $this->get($serviceInfo['handler_service']);
$this->dispatcher->dispatch(CommandEvents::COMMAND_BEFORE_HANDLE, $command);
$response = $handler->run($command, null);
$command->setResponse($response);
$this->dispatcher->dispatch(CommandEvents::COMMAND_AFTER_HANDLE, $command);
if ($command->hasResponse()) {
return $command->getResponse();
}
return $response;
}
/**
* @param array $serviceInfo
* @param Request $request
* @return CommandEvent
* @throws Exception
*/
protected function buildCommand(array $serviceInfo, Request $request)
{
$commandClass = $this->getCommandClass($serviceInfo, $request);
$commandClass = new ReflectionClass($commandClass);
$properties = $commandClass->getProperties();
$requestVars = $serviceInfo['method'] === "POST" ? $request->query : $request->request;
$data = array();
$commandData = array();
foreach ($properties as $prop) {
$propName = $prop->getName();
$propsAnnotations = $this->reader->getPropertyAnnotations($prop);
/** @var RequestParam $annotation */
foreach ($propsAnnotations as $annotation) {
if (!$annotation instanceof RequestParam) {
continue;
}
$value = $requestVars->get($annotation->getName());
if (is_null($value)) {
if ($annotation->isRequired()) {
throw new Exception("No required parameter {$annotation->getName()}");
}
}
$pattern = $annotation->getPattern();
if (isset($pattern) && !preg_match($annotation->getPattern(), $value)) {
throw new Exception("The parameter " . ($annotation->getName()) . " is not matches the requirements");
}
$data[$propName][$annotation->getName()] = $value;
}
$hasRequestParam = !is_null($this->reader->getPropertyAnnotation($prop, RequestParam::class));
if (!$hasRequestParam) {
continue;
}
/** @var Type|null $propType */
$propType = $this->reader->getPropertyAnnotation($prop, Type::class);
$propValue = current($data[$propName]);
if (!is_null($propType)) {
switch ($propType->getType()) {
case 'string':
//$propValue = $propValue;
break;
case 'int':
$propValue = intval($propValue);
break;
case 'boolean':
$propValue = intval($propValue) === 1;
break;
case 'float':
$propValue = floatval($propValue);
break;
case 'array':
$propValue = is_array($propValue) ? $propValue : array($propValue);
break;
default:
$config = new Configuration($propType->getType());
$hydratorClass = $config->createFactory()->getHydratorClass();
$hydrator = new $hydratorClass();
$object = $this->instantiator->instantiate($propType->getType());
$hydrator->hydrate(
$data[$propName],
$object
);
$propValue = $object;
}
}
$commandData[$propName] = $propValue;
}
$config = new Configuration($commandClass->getName());
$hydratorClass = $config->createFactory()->getHydratorClass();
$hydrator = new $hydratorClass();
$command = $this->instantiator->instantiate($commandClass->getName());
$hydrator->hydrate(
$commandData,
$command
);
return $command;
}
/**
* @param array $serviceInfo
* @param Request $request
* @return CommandEventInterface|null
* @throws Exception
*/
protected function getCommandClass(array $serviceInfo, Request $request)
{
$commandClass = null;
if (isset($serviceInfo['command_class'])) {
$commandClass = $serviceInfo['command_class'];
} else if (!is_null($serviceInfo['guesser_service'])) {
/** @var CommandGuesserInterface $guesser */
$guesser = $this->get($serviceInfo['guesser_service']);
$commandClass = $guesser->guess($request, $serviceInfo['method']);
}
if (is_null($commandClass)) {
throw new Exception("No command class found");
}
return $commandClass;
}
protected function get($id)
{
return $this->container->get($id);
}
}
<?php
interface CommandGuesserInterface
{
/**
* @param ParameterBag $params
* @param $httpMethod
* @return CommandEventInterface
*/
public function guess(ParameterBag $params, $httpMethod);
}
<?php
/**
* @Annotation
* @Target({"PROPERTY"})
*/
final class RequestParam
{
/** @var string */
public $value;
/** @var string */
public $pattern;
/** @var bool */
public $required = false;
/**
* @return string
*/
public function getName()
{
return $this->value;
}
/**
* @return string
*/
public function getPattern()
{
if (empty($this->pattern)) {
return null;
}
return '/'.ltrim(trim($this->pattern), '/') . '/';
}
/**
* @return boolean
*/
public function isRequired()
{
return $this->required;
}
}
<?php
class TestController extends Controller
{
/**
* @Route("/api/{service}/{method}")
* @param Request $request
* @param string $service
* @param string $method
* @return array
*/
public function apiAction(Request $request, $service, $method)
{
/**
* @var $dispatcher EventDispatcherInterface
*/
$dispatcher = $this->get('event_dispatcher');
$event = new GetResponseEvent($request, $service, $method);
if ($dispatcher->hasListeners(ApiEvents::FRONT_CONTROLLER_HANDLE)) {
$dispatcher->dispatch(ApiEvents::FRONT_CONTROLLER_HANDLE, $event);
}
if ($event->hasResponse()) {
return $event->getResponse();
}
return new Response("404 Not found", 404);
}
}
<?php
/**
* @Annotation
* @Target({"PROPERTY"})
*/
final class Type
{
public $value;
public function getType()
{
return $this->value;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment