Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Custom annotations in Symfony2

Joshua Thijssen
March 06, 2014
1.4k

Custom annotations in Symfony2

Joshua Thijssen

March 06, 2014
Tweet

Transcript

  1. 2 Joshua Thijssen Freelance consultant, developer and trainer @ NoxLogic

    Founder of the Dutch Web Alliance Development in PHP, Python, C, Java. Lead developer of Saffire. Blog: http://adayinthelifeof.nl Email: [email protected] Twitter: @jaytaph
  2. 7 ➡ @ - notation, cause other languages do too

    :) ➡ Userland only :( ➡ Reflection :( ➡ Different readers :(
  3. 14 ➡ Read / parse annotations from classes / methods

    / properties. ➡ Places them in data objects (POPOs) ➡ Caching etc.
  4. 15 1 <?php 2 3 // vendor/NoxLogic/Annotations/Append.php 4 5 namespace

    NoxLogic\Annotations; 6 use Doctrine\Common\Annotations\Annotation; 7 8 /** 9 * @Annotation 10 */ 11 class Append { 12 public $value; 13 public $repeat = 8; 14 public $priority = 99; 15 } 16 simple annotation based on public properties
  5. 16 1 <?php 2 3 // vendor/NoxLogic/Annotations/Append.php 4 5 namespace

    NoxLogic\Annotations; 6 use Doctrine\Common\Annotations\Annotation; 7 8 /** 9 * @Annotation 10 */ 11 class Append { 12 protected $value; 13 protected $repeats; 14 protected $priority; 15 16 function __construct($options) { 17 $this->value = $options['value']; 18 $this->repeats = isset($options['repeat']) ? $options['repeat'] : 1; 19 $this->priority = isset($options['priority']) ? $options['priority'] : 100; 20 } 21 22 } 23 simple annotation based on __construct values
  6. 17 1 <?php 2 3 // vendor/NoxLogic/Annotations/Prepend.php 4 5 namespace

    NoxLogic\Annotations; 6 use Doctrine\Common\Annotations\Annotation; 7 8 /** 9 * @Annotation 10 * @Target({"METHOD"}) 11 */ 12 class Prepend extends Annotation { 13 public $value; 14 protected $repeat = 1; 15 protected $priority = 100; 16 17 function setValue($value) { 18 $this->value = $value; 19 } 20 21 function setRepeat($repeat) { 22 $this->repeats = $repeat; 23 } 24 25 function setPriority($priority) { 26 $this->priority = $priority; 27 } 28 29 } 30 simple annotation based on getters/setters
  7. 18 1 <?php 2 3 // vendor/NoxLogic/Foo.php 4 5 namespace

    NoxLogic; 6 7 use \NoxLogic\Annotations as NoxAnnotations; 8 9 class Foo { 10 11 /** 12 * @author Joshua Thijssen 13 * 14 * @NoxAnnotations\Prepend("text before", repeat = 3) 15 * @NoxAnnotations\Prepend("more text before") 16 * @NoxAnnotations\Append("some text after") 17 */ 18 function output($string) { 19 return "[".$string."]"; 20 } 21 22 } annotation usage
  8. 19 1 <?php 2 3 use Doctrine\Common\Annotations\AnnotationReader; 4 use Doctrine\Common\Annotations\AnnotationRegistry;

    5 6 require_once "vendor/autoload.php"; 7 8 9 AnnotationRegistry::registerAutoloadNamespace("\\NoxLogic\\", "vendor"); 10 $reader = new AnnotationReader(); 11 12 13 $class = new ReflectionClass("\\NoxLogic\\Foo"); 14 foreach ($class->getMethods() as $method) { 15 16 $annotations = $reader->getMethodAnnotations($method); 17 foreach ($annotations as $annotation) { 18 print_r ($annotation); 19 } 20 } 21 main application Can’t be PSR0 loader. Need silence!
  9. 20 1 $ php annotations.php 2 NoxLogic\Annotations\Prepend Object 3 (

    4 [value] => text before 5 [repeat:protected] => 3 6 [priority:protected] => 100 7 ) 8 NoxLogic\Annotations\Prepend Object 9 ( 10 [value] => more text before 11 [repeat:protected] => 1 12 [priority:protected] => 100 13 ) 14 NoxLogic\Annotations\Append Object 15 ( 16 [value:protected] => some text after 17 [repeats:protected] => 1 18 [priority:protected] => 100 19 )
  10. 21 1 <?php 2 3 // vendor/NoxLogic/Foo.php 4 5 namespace

    NoxLogic; 6 7 use \NoxLogic\Annotations as NoxAnnotations; 8 9 class Foo { 10 11 /** 12 * @author Joshua Thijssen 13 * 14 * @NoxAnnotations\Prepend("text before", repeat = 3) 15 * @NoxAnnotations\Prepend("more text before") 16 * @NoxAnnotations\Append("some text after") 17 */ 18 function output($string) { 19 return "[".$string."]"; 20 } 21 22 } annotation usage Add use statements! (can mess up your IDE) Ignored Annotations
  11. 22 private static $globalIgnoredNames = array( 'access'=> true, 'author'=> true,

    'copyright'=> true, 'deprecated'=> true, 'example'=> true, 'ignore'=> true, 'internal'=> true, 'link'=> true, 'see'=> true, 'since'=> true, 'tutorial'=> true, 'version'=> true, 'package'=> true, 'subpackage'=> true, 'name'=> true, 'global'=> true, 'param'=> true, 'return'=> true, 'staticvar'=> true, 'category'=> true, 'staticVar'=> true, 'static'=> true, 'var'=> true, 'throws'=> true, 'inheritdoc'=> true, 'inheritDoc'=> true, 'license'=> true, 'todo'=> true, 'TODO'=> true, 'deprec'=> true, 'property' => true, 'method' => true, 'abstract'=> true, 'exception'=> true, 'magic' => true, 'api' => true, 'final'=> true, 'filesource'=> true, 'throw' => true, 'uses' => true, 'usedby'=> true, 'private' => true, 'Annotation' => true, 'override' => true, 'codeCoverageIgnore' => true, 'codeCoverageIgnoreStart' => true, 'codeCoverageIgnoreEnd' => true, 'Required' => true, 'Attribute' => true, 'Attributes' => true, 'Target' => true, 'SuppressWarnings' => true, 'ingroup' => true, 'code' => true, 'endcode' => true, 'package_version' => true, 'fixme' => true ); Doctrine\Common\Annotations\AnnotationReader
  12. 23 1 $ php annotations.php 2 NoxLogic\Annotations\Prepend Object 3 (

    4 [value] => text before 5 [repeat:protected] => 3 6 [priority:protected] => 100 7 ) 8 NoxLogic\Annotations\Prepend Object 9 ( 10 [value] => more text before 11 [repeat:protected] => 1 12 [priority:protected] => 100 13 ) 14 NoxLogic\Annotations\Append Object 15 ( 16 [value:protected] => some text after 17 [repeats:protected] => 1 18 [priority:protected] => 100 19 )
  13. ➡ Does NOT contain logic to does “magic” execution. ➡

    “Prepend” and “Append” are just names. 24
  14. 1. Create annotation class 2. Create event listener(s) 3. Service

    configuration 4. ??? 5. Profit! 27 World Domination Plan (v5.3 beta)
  15. 28 1 <?php 2 3 namespace NoxLogic\DemoBundle\Service\Annotation; 4 5 /**

    6 * @Annotation 7 */ 8 class Reverse { 9 10 } simple annotation without data values
  16. 29 1 <?php 2 3 namespace NoxLogic\DemoBundle\EventListener; 4 5 use

    NoxLogic\DemoBundle\Service\Annotation\Reverse; 6 use Symfony\Component\HttpKernel\Event\FilterControllerEvent; 7 8 class AnnotationListener { 9 10 /** @var \Doctrine\Common\Annotations\Reader */ 11 protected $reader; 12 13 function __construct(\Doctrine\Common\Annotations\Reader $reader) { 14 // Inject a annotation reader 15 $this->reader = $reader; 16 } 17 ... annotation listener 1/2
  17. 30 .... 18 public function onKernelController(FilterControllerEvent $event) { 19 //

    Skip if we aren't a controller 20 if (! is_array($controller = $event->getController())) { 21 return; 22 } 23 24 $object = new \ReflectionObject($controller[0]); 25 $method = $object->getMethod($controller[1]); 26 27 foreach ($this->reader->getMethodAnnotations($method) as $annotation) { 28 if ($annotation instanceof Reverse) { 29 $r = new \ReflectionMethod($object, $method); 30 foreach ($r->getParameters() as $param) { 31 $name = $param->getName(); 32 33 $request = $event->getRequest(); 34 $request->attributes->set($name, strrev($request->attributes->get($name))); 35 } 36 } 37 } 38 } 39 40 } annotation listener 2/2
  18. 31 1 parameters: 2 noxlogic_demo_bundle.annotation.listener.class: NoxLogic\DemoBundle\EventListener\AnnotationListener 3 4 services: 5

    noxlogic_demo_bundle.annotation.listener: 6 class : %noxlogic_demo_bundle.annotation.listener.class% 7 arguments: [ @annotation_reader ] 8 tags: 9 - { name: kernel.event_listener, event: kernel.controller, method: onKernelController, priority: 10 } service configuration
  19. 32 1 <?php 2 3 namespace NoxLogic\DemoBundle\Controller; 4 5 use

    Symfony\Bundle\FrameworkBundle\Controller\Controller; 6 use NoxLogic\DemoBundle\Service\Annotation as NoxAnnotation; 7 8 class DefaultController extends Controller 9 { 10 11 /** 12 * @NoxAnnotation\Reverse() 13 */ 14 public function indexAction($name) 15 { 16 return $this->render('NoxLogicDemoBundle:Default:index.html.twig', array('name' => $name)); 17 } 18 } hello dlrow controller
  20. 33 1 <?php 2 3 namespace NoxLogic\DemoBundle\Controller; 4 5 use

    Symfony\Bundle\FrameworkBundle\Controller\Controller; 6 use NoxLogic\DemoBundle\Service\Annotation as NoxAnnotation; 7 8 class DefaultController extends Controller 9 { 10 11 /** 12 * @NoxAnnotation\Reverse() 13 * @NoxAnnotation\Reverse() 14 */ 15 public function indexAction($name) 16 { 17 return $this->render('NoxLogicDemoBundle:Default:index.html.twig', array('name' => $name)); 18 } 19 } hello world multiple annotations
  21. 34 1 foreach ($this->reader->getMethodAnnotations($method) as $annotation) { 2 if ($annotation

    instanceof Reverse) { 3 $r = new \ReflectionMethod($controller[0], $controller[1]); 4 foreach ($r->getParameters() as $param) { 5 $name = $param->getName(); 6 7 $request = $event->getRequest(); 8 $request->attributes->set($name, strrev($request->attributes->get($name))); 9 } 10 } 11 12 if ($annotation instanceof Uppercase) { 13 $r = new \ReflectionMethod($controller[0], $controller[1]); 14 foreach ($r->getParameters() as $param) { 15 $name = $param->getName(); 16 17 $request = $event->getRequest(); 18 $request->attributes->set($name, strtoupper($request->attributes->get($name))); 19 } 20 } 21 22 if ($annotation instanceof .... complex listener
  22. 36 ➡ Move annotations to separate listeners ➡ Uppercase Listener

    ➡ Reverse Listener ➡ Move “business logic” away into separate services (upperCaseService, reverseService, transformService)
  23. 38 1 <?php 2 3 namespace NoxLogic\DemoBundle\Controller; 4 5 use

    Symfony\Bundle\FrameworkBundle\Controller\Controller; 6 use NoxLogic\DemoBundle\Service\Annotation as NoxAnnotations; 7 8 class DefaultController extends Controller 9 { 10 11 /** 12 * @NoxAnnotations\Header(name="X-Some-Header", value="Data"); 13 * @NoxAnnotations\Header(name="X-AnotherHeader", value="123456"); 14 */ 15 public function indexAction($name) 16 { 17 return $this->render('NoxLogicDemoBundle:Default:index.html.twig', array('name' => $name)); 18 } 19 } header annotation usage
  24. 41 1 public function onKernelController(FilterControllerEvent $event) { 2 // Skip

    if we aren't a controller 3 if (! is_array($controller = $event->getController())) { 4 return; 5 } 6 7 $object = new \ReflectionObject($controller[0]); 8 $method = $object->getMethod($controller[1]); 9 10 foreach ($this->reader->getMethodAnnotations($method) as $annotation) { 11 if (! $annotation instanceof Header) continue; 12 13 $request = $event->getRequest(); 14 $request->attributes->set('_header', $annotation); 15 } 16 } storing the annotation inside the $request
  25. 42 1 public function onKernelResponse(FilterResponseEvent $event) { 2 $request =

    $event->getRequest(); 3 4 $annotation = $request->attributes->get('_header', null); 5 if (! $annotation) return; 6 7 $response = $event->getResponse(); 8 $response->headers->set($annotation->getName(), $annotation->getValue()); 9 } retrieving the annotation from the $request
  26. 44 1 /** 2 * @Annotation 3 */ 4 class

    Header extends ConfigurationAnnotation { 5 6 protected $name; 7 protected $value; 8 9 public function getAliasName() 10 { 11 return "header"; 12 } 13 14 public function allowArray() 15 { 16 return true; 17 } using sensioFrameworkExtraBundle configurationAnnotation Calls setName() setValue(), stores annotation info inside request attribute field named “_header” and allows multiple annotations.
  27. 45 1 public function onKernelResponse(FilterResponseEvent $event) { 2 $request =

    $event->getRequest(); 3 4 $annotations = $request->attributes->get('_header', null); 5 if (! $annotations) return; 6 7 $response = $event->getResponse(); 8 foreach ($annotations as $annotation) { 9 $response->headers->set($annotation->getName(), $annotation->getValue()); 10 } 11 } auto injects (multiple) annotations inside $request
  28. ➡ Uses kernel.controller (prio: 0) ➡ Does work when kernel.controller

    has been called. ➡ Does not work on kernel.request etc. 46
  29. 48 ➡ Modifies mostly setup of a service. ➡ Tagging,

    injection of services, security permissions etc. ➡ Pretty much you are changing the Dependency Injection Container.
  30. 1. Create annotation class 2. Add compiler pass that checks

    and tags annotated services. 3. Add compiler pass that collects tagged services and does the magic 49 World Domination Plan (v5.3 beta)
  31. ➡ Cannot inject TOP_SECRET into PUBLIC or SECRET. ➡ Can

    inject PUBLIC or SECRET into TOP_SECRET. ➡ Not annotated, we can inject it in any securitylevel. 53
  32. 1 <?php 2 3 namespace NoxLogic\DefaultBundle\Service; 4 5 use \NoxLogic\DefaultBundle\Annotation\SecurityLevel;

    6 7 /** 8 * @SecurityLevel("PUBLIC") 9 */ 10 class PublicService { 11 12 /** 13 * AnotherPublicService 14 */ 15 public $anotherPublicService; 16 17 /** 18 * PrivateService // This should not be able to work! 19 */ 20 public $privateService; 21 22 } 54
  33. 1 <?php 2 3 namespace NoxLogic\DefaultBundle\Service; 4 5 use \NoxLogic\DefaultBundle\Annotation\SecurityLevel;

    6 7 /** 8 * @SecurityLevel("PUBLIC") 9 */ 10 class AnotherPublicService { 11 12 /** 13 * PublicService 14 */ 15 protected $publicService; 16 17 18 function setPublicService($publicService) { 19 $this->publicService = $publicService; 20 } 21 22 } 55
  34. 1 <?php 2 3 /* 4 * Everything in this

    service is highly classified! 5 */ 6 7 namespace NoxLogic\DefaultBundle\Service; 8 9 use \NoxLogic\DefaultBundle\Annotation\SecurityLevel; 10 11 /** 12 * @SecurityLevel("TOP SECRET") 13 */ 14 class PrivateService { 15 16 /** 17 * @var anotherPrivateService 18 */ 19 public $anotherPrivateService; 20 } 56
  35. 1 <?php 2 3 namespace NoxLogic\DefaultBundle\Service; 4 5 use \NoxLogic\DefaultBundle\Annotation\SecurityLevel;

    6 7 /** 8 * @SecurityLevel("SECRET") 9 */ 10 class AnotherPrivateService { 11 12 /** 13 * PublicService 14 */ 15 protected $publicService; 16 17 function __construct($publicService) { 18 $this->publicService = $publicService; 19 } 20 21 } 57
  36. 1 parameters: 2 noxlogic.default.service.public.class: NoxLogic\DefaultBundle\Service\PublicService 3 noxlogic.default.service.anotherpublic.class: NoxLogic\DefaultBundle\Service\AnotherPublicService 4 noxlogic.default.service.private.class:

    NoxLogic\DefaultBundle\Service\PrivateClass 5 noxlogic.default.service.anotherprivate.class: NoxLogic\DefaultBundle\Service\AnotherPrivateService 6 7 services: 8 noxlogic.default.service.public: 9 class: %noxlogic.default.service.public.class% 10 properties: 11 anotherPublicService: @noxlogic.default.service.anotherpublic 12 privateService: @noxlogic.default.service.private 13 14 noxlogic.default.service.anotherpublic: 15 class: %noxlogic.default.service.anotherpublic.class% 16 calls: 17 - [ setPublicService, [ @noxlogic.default.service.public ] ] 18 19 noxlogic.default.service.private: 20 class: %noxlogic.default.service.private.class% 21 properties: 22 anotherPrivateService: @noxlogic.default.service.anotherprivate 23 24 noxlogic.default.service.anotherprivate: 25 class: %noxlogic.default.service.anotherprivate.class% 26 arguments: [ @noxlogic.default.service.public ] 58 Services using other services, constructor, setter and property injection
  37. 60 1 /** 2 * @Annotation 3 * @Target("CLASS") 4

    */ 5 class SecurityLevel { 6 7 const LEVEL_PUBLIC = 1; 8 const LEVEL_SECRET = 2; 9 const LEVEL_TOP_SECRET = 4; 10 11 protected $mapping = array( 12 "PUBLIC" => self::LEVEL_PUBLIC, 13 "SECRET" => self::LEVEL_SECRET, 14 "TOP SECRET" => self::LEVEL_TOP_SECRET, 15 ); 16 17 protected $level = 0; 18 19 public function __construct($value) { 20 $value = array_shift($value); 21 if (array_key_exists($value, $this->mapping)) { 22 $this->level = $this->mapping[$value]; 23 } else { 24 throw new \Exception("Unknown security level specified. Use: PUBLIC, SECRET or TOP_SECRET"); 25 } 26 } 27 28 function getLevel() { 29 return $this->level; 30 } 31 32 }
  38. 61 1 class SecurityLevelCollectorPass implements CompilerPassInterface 2 { 3 4

    public function process(ContainerBuilder $container) 5 { 6 // Can't inject? 7 $reader = $container->get("annotation_reader"); 8 9 foreach ($container->getDefinitions() as $id => $definition) { 10 try { 11 $class = new \ReflectionClass($definition->getClass()); 12 } catch (\ReflectionException $e) { 13 continue; 14 } 15 16 foreach ($reader->getClassAnnotations($class) as $annotation) { 17 if (! $annotation instanceOf \NoxLogic\DefaultBundle\Annotation\SecurityLevel) { 18 continue; 19 } 20 21 $definition->securityLevel = $annotation; 22 } 23 } 24 }
  39. 62 1 class SecurityLevelCheckerPass implements CompilerPassInterface 2 { 3 /**

    @var ContainerBuilder */ 4 protected $container; 5 6 public function process(ContainerBuilder $container) 7 { 8 $this->container = $container; 9 10 foreach ($this->container->getDefinitions() as $id => $definition) { 11 if (! property_exists($definition, 'securityLevel')) continue; 12 13 $this->checkSecurityClearance($definition, $definition->getArguments()); 14 $this->checkSecurityClearance($definition, $definition->getMethodCalls()); 15 $this->checkSecurityClearance($definition, $definition->getProperties()); 16 } 17 } ...
  40. 63 1 function checkSecurityClearanceForArgument(Definition $definition, Reference $reference, Definition $targetDefinition) {

    2 // No target is ok 3 if (! $targetDefinition) return; 4 5 // No securitylevel on the target is ok 6 if (! isset($targetDefinition->securityLevel)) { 7 return; 8 } 9 10 if ($definition->securityLevel->getLevel() > $targetDefinition->securityLevel->getLevel()) { 11 throw new SecurityLevelException("You cannot inject " . $targetDefinition->getClass() . " into the more secured service " . $definition->getClass()); 12 } 13 }
  41. 64 1 <?php 2 3 namespace NoxLogic\DefaultBundle; 4 5 use

    NoxLogic\DefaultBundle\DependencyInjection\Compiler\SecurityLevelCheckerPass; 6 use NoxLogic\DefaultBundle\DependencyInjection\Compiler\SecurityLevelCollectorPass; 7 use Symfony\Component\DependencyInjection\Compiler\PassConfig; 8 use Symfony\Component\DependencyInjection\ContainerBuilder; 9 use Symfony\Component\HttpKernel\Bundle\Bundle; 10 11 class NoxLogicDefaultBundle extends Bundle 12 { 13 public function build(ContainerBuilder $container) 14 { 15 $config = $container->getCompiler()->getPassConfig(); 16 $config->addPass(new SecurityLevelCollectorPass(), PassConfig::TYPE_AFTER_REMOVING); 17 $config->addPass(new SecurityLevelCheckerPass(), PassConfig::TYPE_AFTER_REMOVING); 18 } 19 }
  42. 65

  43. 68 Find me on twitter: @jaytaph Find me for consultancy

    and training: www.noxlogic.nl Find me on email: [email protected] Find me for blogs: www.adayinthelifeof.nl