├── README.md ├── api.info.yml ├── api.routing.yml ├── api.services.yml ├── composer.json └── src ├── Controller ├── AbstractTransformController.php ├── ArticleController.php └── SimpleController.php ├── Form └── Type │ └── ArticleType.php ├── Serializer └── ArraySerializer.php └── Transformer └── ArticleTransformer.php /README.md: -------------------------------------------------------------------------------- 1 | # Drupal Example API Module 2 | 3 | An *example* module which demonstrates on how to create custom API endpoints with the use of: 4 | 5 | - [Controller Annotations](https://github.com/mediamonks/drupal-controller-annotations) 6 | - [MediaMonks Rest API](https://github.com/mediamonks/php-rest-api) 7 | - [Fractal](http://fractal.thephpleague.com) 8 | - [Symfony Form](https://symfony.com/doc/current/components/form.html) 9 | - [Symfony Validator](https://symfony.com/doc/current/components/validator.html) 10 | - [Controllers as Services](https://symfony.com/doc/current/controller/service.html) 11 | - [Service Autowiring](https://symfony.com/doc/current/service_container/autowiring.html) 12 | 13 | ## Requirements 14 | 15 | - PHP 7.2+ 16 | - Drupal 8.6+ 17 | 18 | ## Installation 19 | 20 | Add the following repository to your composer.json file under *repositories*: 21 | 22 | ``` 23 | { 24 | "type": "vcs", 25 | "url": "https://github.com/slootjes/drupal-example-api-module" 26 | } 27 | ``` 28 | 29 | and then add the module with composer: 30 | 31 | ``` 32 | composer require drupal/example-api 33 | ``` 34 | 35 | Then enable the module *Controller Annotations* and *Example API* module like you're used to and you're good to go. 36 | 37 | ## Disclaimer 38 | 39 | This module is created for *educational and research purposes*. Most examples are not following Drupal security best practices and thus are not meant to be used as-is. I cannot be held responsible for any issues that occur from you using these examples. 40 | -------------------------------------------------------------------------------- /api.info.yml: -------------------------------------------------------------------------------- 1 | name: API 2 | description: Example API 3 | core: 8.x 4 | package: Example 5 | type: module 6 | -------------------------------------------------------------------------------- /api.routing.yml: -------------------------------------------------------------------------------- 1 | api_annotations: 2 | path: /api 3 | options: 4 | type: annotation 5 | module: api 6 | -------------------------------------------------------------------------------- /api.services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | # rest api 3 | MediaMonks\RestApi\EventSubscriber\RestApiEventSubscriber: 4 | autowire: true 5 | tags: 6 | - { name: event_subscriber } 7 | 8 | MediaMonks\RestApi\Request\PathRequestMatcher: 9 | public: false 10 | arguments: 11 | - '/api' 12 | 13 | MediaMonks\RestApi\Request\RequestTransformer: 14 | public: false 15 | autowire: true 16 | 17 | MediaMonks\RestApi\Response\ResponseTransformer: 18 | public: false 19 | autowire: true 20 | 21 | MediaMonks\RestApi\Serializer\JsonSerializer: 22 | public: false 23 | 24 | MediaMonks\RestApi\Model\ResponseModel: 25 | public: false 26 | 27 | MediaMonks\RestApi\Model\ResponseModelFactory: 28 | public: false 29 | autowire: true 30 | 31 | # fractal 32 | League\Fractal\Manager: 33 | calls: 34 | - [setSerializer, ['@Drupal\api\Serializer\ArraySerializer']] 35 | 36 | Drupal\api\Serializer\ArraySerializer: ~ 37 | 38 | Drupal\api\Transformer\ArticleTransformer: ~ 39 | 40 | # form 41 | Symfony\Component\Validator\Validator\ValidatorInterface: 42 | factory: ['Symfony\Component\Validator\Validation', createValidator] 43 | 44 | Symfony\Component\Form\FormFactoryInterface: 45 | factory: 'Symfony\Component\Form\FormFactoryBuilder:getFormFactory' 46 | 47 | Symfony\Component\Form\Extension\Core\CoreExtension: ~ 48 | 49 | Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationExtension: ~ 50 | 51 | Symfony\Component\Form\Extension\Validator\ValidatorExtension: 52 | autowire: true 53 | 54 | Symfony\Component\Form\FormFactoryBuilder: 55 | calls: 56 | - [addExtension, ['@Symfony\Component\Form\Extension\Core\CoreExtension']] 57 | - [addExtension, ['@Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationExtension']] 58 | - [addExtension, ['@Symfony\Component\Form\Extension\Validator\ValidatorExtension']] 59 | 60 | # autowiring aliases 61 | Drupal\Core\Entity\EntityTypeManagerInterface: 62 | alias: 'entity_type.manager' 63 | 64 | # controllers 65 | Drupal\api\Controller\ArticleController: 66 | autowire: true 67 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drupal/example-api", 3 | "type": "drupal-module", 4 | "homepage": "https://mediamonks.com/", 5 | "license": "GPL-2.0", 6 | "authors": [ 7 | { 8 | "name": "Robert Slootjes", 9 | "email": "robert@mediamonks.com", 10 | "homepage": "https://github.com/slootjes" 11 | } 12 | ], 13 | "require": { 14 | "php": "^7.2", 15 | "mediamonks/rest-api": "^1.0", 16 | "drupal/controller_annotations": "^1.0", 17 | "league/fractal": "^0.17.0", 18 | "symfony/form": "^3.4", 19 | "symfony/validator": "^3.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Controller/AbstractTransformController.php: -------------------------------------------------------------------------------- 1 | entityTypeManager = $entityTypeManager; 55 | $this->fractalManager = $fractalManager; 56 | $this->formFactory = $formFactory; 57 | $this->transformer = $transformer; 58 | } 59 | 60 | /** 61 | * @param Request $request 62 | * @param string $type 63 | * 64 | * @return OffsetPaginatedResponse 65 | */ 66 | protected function getPaginatedResponse( 67 | Request $request, 68 | string $type 69 | ): OffsetPaginatedResponse { 70 | $offset = $request->query->getInt('offset'); 71 | $limit = $request->query->getInt('limit', 25); 72 | 73 | $nodeStorage = $this->getNodeStorage(); 74 | 75 | $query = $nodeStorage->getQuery() 76 | ->condition('type', $type) 77 | ->sort('created', 'DESC'); 78 | 79 | $countQuery = clone $query; 80 | $count = (int)$countQuery->count()->execute(); 81 | 82 | $ids = $query->range($offset, $limit)->execute(); 83 | 84 | return new OffsetPaginatedResponse( 85 | $this->transformCollection($nodeStorage->loadMultiple($ids)), 86 | $offset, 87 | $limit, 88 | $count 89 | ); 90 | } 91 | 92 | /** 93 | * @return EntityStorageInterface 94 | */ 95 | protected function getNodeStorage() 96 | { 97 | return $this->entityTypeManager->getStorage('node'); 98 | } 99 | 100 | /** 101 | * @param ContentEntityInterface $entity 102 | * 103 | * @return array 104 | */ 105 | protected function transformSingle(ContentEntityInterface $entity): array 106 | { 107 | return $this->transformResource(new Item($entity, $this->transformer)); 108 | } 109 | 110 | /** 111 | * @param array $entities 112 | * 113 | * @return array 114 | */ 115 | protected function transformCollection(array $entities): array 116 | { 117 | return $this->transformResource( 118 | new Collection($entities, $this->transformer) 119 | ); 120 | } 121 | 122 | /** 123 | * @param ResourceInterface $resource 124 | * 125 | * @return array 126 | */ 127 | protected function transformResource(ResourceInterface $resource): array 128 | { 129 | return $this->fractalManager->createData($resource)->toArray(); 130 | } 131 | 132 | /** 133 | * @param Request $request 134 | * @param string $formType 135 | * @param string $nodeType 136 | * 137 | * @return Response 138 | * @throws FormValidationException 139 | */ 140 | protected function handleForm(Request $request, string $formType, string $nodeType): Response 141 | { 142 | $form = $this->formFactory->create($formType); 143 | $form->submit($request->request->all()); 144 | if (!$form->isValid()) { 145 | throw new FormValidationException($form); 146 | } 147 | 148 | $node = $this->getNodeStorage()->create( 149 | $form->getData() + ['type' => $nodeType] 150 | ); 151 | $node->save(); 152 | 153 | return new Response($node->id(), Response::HTTP_CREATED); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Controller/ArticleController.php: -------------------------------------------------------------------------------- 1 | getPaginatedResponse($request, self::ENTITY_TYPE); 60 | } 61 | 62 | /** 63 | * @Route("/{article}") 64 | * @Method("GET") 65 | * @Security(access=true) 66 | * @ParamConverter("article", 67 | * options={"bundle": ArticleController::ENTITY_TYPE} 68 | * ) 69 | * 70 | * @param Node $article 71 | * 72 | * @return array 73 | */ 74 | public function showAction(Node $article): array 75 | { 76 | return $this->transformSingle($article); 77 | } 78 | 79 | /** 80 | * @Route() 81 | * @Method("POST") 82 | * @Security(access=true) 83 | * 84 | * @param Request $request 85 | * 86 | * @return Response 87 | * @throws FormValidationException 88 | */ 89 | public function createAction(Request $request): Response 90 | { 91 | return $this->handleForm( 92 | $request, 93 | ArticleType::class, 94 | self::ENTITY_TYPE 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Controller/SimpleController.php: -------------------------------------------------------------------------------- 1 | add('title', TextType::class, 23 | [ 24 | 'constraints' => [ 25 | new NotBlank(), 26 | new Length(['min' => 3, 'max' => 100]) 27 | ], 28 | ] 29 | ) 30 | ->add('body', TextareaType::class, 31 | [ 32 | 'constraints' => [ 33 | new NotBlank(), 34 | new Length(['min' => 10]) 35 | ], 36 | ] 37 | ); 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/Serializer/ArraySerializer.php: -------------------------------------------------------------------------------- 1 | null]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Transformer/ArticleTransformer.php: -------------------------------------------------------------------------------- 1 | (int)$article->id(), 20 | 'title' => $article->get('title')->value, 21 | 'body' => $article->get('body')->value, 22 | ]; 23 | } 24 | } 25 | --------------------------------------------------------------------------------