Created
April 22, 2015 12:30
-
-
Save zemd/1e31675a012598bc24e9 to your computer and use it in GitHub Desktop.
API DDD prototype
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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) | |
); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
interface CommandGuesserInterface | |
{ | |
/** | |
* @param ParameterBag $params | |
* @param $httpMethod | |
* @return CommandEventInterface | |
*/ | |
public function guess(ParameterBag $params, $httpMethod); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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