From 10ce6744610b2576fa35728e84f2a09fe6135f5e Mon Sep 17 00:00:00 2001 From: Pete Inge Date: Wed, 11 Jun 2025 09:40:59 -0400 Subject: [PATCH 01/12] fix: type hinting of class vars --- src/Controller/ApiControllerBase.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controller/ApiControllerBase.php b/src/Controller/ApiControllerBase.php index e86e370..28264ef 100644 --- a/src/Controller/ApiControllerBase.php +++ b/src/Controller/ApiControllerBase.php @@ -174,14 +174,14 @@ class ApiControllerBase extends ControllerBase implements ApiControllerInterface /** * Limit the number of results. * - * @var int + * @var string */ protected $prev = ""; /** * Limit the number of results. * - * @var int + * @var string */ protected $next = ""; From 286447c18d2852a384df4e8ec463a304fa68fbf1 Mon Sep 17 00:00:00 2001 From: Pete Inge Date: Sat, 21 Jun 2025 11:09:05 -0400 Subject: [PATCH 02/12] chore: cleanup .DS_store files --- modules/bc_api_docs/assets/dist/.DS_Store | Bin 6148 -> 0 bytes modules/bc_api_docs/assets/dist/js/.DS_Store | Bin 6148 -> 0 bytes modules/bc_api_example/config/.DS_Store | Bin 6148 -> 0 bytes modules/bc_api_logger/config/.DS_Store | Bin 6148 -> 0 bytes tests/src/.DS_Store | Bin 6148 -> 0 bytes 5 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 modules/bc_api_docs/assets/dist/.DS_Store delete mode 100644 modules/bc_api_docs/assets/dist/js/.DS_Store delete mode 100644 modules/bc_api_example/config/.DS_Store delete mode 100644 modules/bc_api_logger/config/.DS_Store delete mode 100644 tests/src/.DS_Store diff --git a/modules/bc_api_docs/assets/dist/.DS_Store b/modules/bc_api_docs/assets/dist/.DS_Store deleted file mode 100644 index cbbf28ba489708c403b2e3370b876a9db4bb2a7c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}&BV5Z*=95@Y0GqQ*-v-Y}uYpci9`nb{WPPFL!;5u{>kU07cB?pqU~#jyfS3?O6ciIENPs z59~Lu9R?H8@w-Wlv5)7?=i zmS${Uv>jrA82Ebz@P05s5p9jBLV0ySBTE3lEV!kBjkN^km>O-3sX`b5;VKnSrE*ab0|Hy)xGo3YV+H{H6?N+*U{}F+dD_XJAfs^Z5QB|6KonPof?% zKn(mV26(pORLXE9{kBdV4&Pc6v<{9 diff --git a/modules/bc_api_docs/assets/dist/js/.DS_Store b/modules/bc_api_docs/assets/dist/js/.DS_Store deleted file mode 100644 index ac4d3e59e9ee81ac8947cf84dbb83695def33da6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK&1%Ci43@SY0=x8B*u}4qH!vl8g22v&ru%_~WFtNF=G)$JPqR;!TAJ0c%NS!w z!Tw}hlI;&-TSUa`eOD7LiKs#mWKjwt-Nm6RGhP5W$C&7u9_aA$Shs~hf3Zu}UeQQz zw4({%Y5m)wAI}ziVbc#y)9t4%(Z~Dk>*2l>rysg+ZN{g%{W90tw5~b>&VV!E3^)Tn z&H(Ogk?Ka#YiGb2a0X5c$oUXZ1fyY6%tr^RoB{yl8Jz{X|KH)4DJJ=I7jHQO z&cHuofEP{Mtg$J(TQ9aJcWpq~pooZF5(NT%_6R^j&XH9f)bSuX?4n^(lqk}l-GTlP M$b@+14EzEEpA~H_H~;_u diff --git a/modules/bc_api_example/config/.DS_Store b/modules/bc_api_example/config/.DS_Store deleted file mode 100644 index 7d6a84581cca261a1b38c701c1a1e19a87ea57fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKy=ucS5WZ_O7`kNas8{e4R0QVs^@{sI$&b~<3e5bpHxDj+IrIb66 zzLV}p@&}f5M8vD(&=M_)s6i8CQ7R(SRnv_*FMwQfB-+z6?RGD1UrF>Ar)2L9C7S3! zZ@}~Zw`rU%HhkqXrmh=~hb__jhP4 zJ6oi=Q}o^$a0Z-#GXruy1T?{Dm=(*>fv%JQKzT+NfiAU##00}=m=$4xu!aIPl&!>I z4aa;izi5~hHJsRr54M#*ix)1dBY!CF#8J_EXTTYlGjOZJrQH8({4&)de;(o^XTTZw zXAJP7>$?>`%I?;y&y%}0pgp09h+h^30)6%fz(CHC`(jjk5FLKeFe^$HnJ?i$e+U#p KymJPAfq@gq#4XeS diff --git a/modules/bc_api_logger/config/.DS_Store b/modules/bc_api_logger/config/.DS_Store deleted file mode 100644 index 85dc1102ea62ab4988b9804027443cf62eff9084..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK&1%~~5T141#+yP>p@#%=Sm>b_2W)5?ABCIr6iBH;bI6Y)AyEs&DoApCFb0ta z=mWIZHhGY=@6cnPqUX-+u1gZP$AqNJ!0b1gota&ID|WR2Ao`=Y3*Z4j3ze`TvH6Qo zKk1w_oQF`D8X;tmMdR`~%I2ckaTgh&wOfP~G?Y-mrS;1r?D-WPXK7h=IzL2Xq50t9 zVoNSbDIZID5DfEiP=(c~?1#m`TMg1UiNazxjjQ2KqBpbTB+=W&R#+v)9tH2@d4>Vj z4x{&JG9GwOU+20CQ=Js1o~^OJo9Z|p_w$jCOB3&k8~bfo4Y7`V=BGamqd1{~w=$Wm z^?JRw>h@kv+iJ4D-f62Bue#Hzl&zKLZ{8h_PCtJ7{N?M}`NcPr1_HlXBTpR8;5!=E zI5`Olo#pxi_RO+o(TvOhGr$boKL*@R)_QdRrp_~F2AF{xW`Onwg-Yl$s7HyP*g(I{K9f z2jN@fmKk6Mt}?J-A3Zw%H~)SAzna8P%m6d+Uojw>8^J~&U&)=Vb1z3{twKFQC84<5 m;-3^W%u$T7bQCY68bQAz1JQR_TSN~E{|FcwxM2ovm4V;dx?_|8 diff --git a/tests/src/.DS_Store b/tests/src/.DS_Store deleted file mode 100644 index ba4994a5eb30e97bf8aa5be9aff760c5e2e89d25..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK!EV$r5FKw9Sho@i5{Dx7V2J~lJ#587IYvv{6H+Tu(E}~pghXtl$gZ+U_pn+i z;sf{qxYG7NT0Vj!Kf#?D+l#2STq}xZr16`KXU5KRWXAv?26eg)5CA|2m9QnTxkjj; zbU|9)Lnv&G3UV-@A%RdH312XQ$J)}lPklDOQ@(`vMr>8&C=$@EV7Dz38fEd}ox zQ(%CzU7$F*6*sPy%)2tnr>|Lx~l(teKwP_^XSR;>v#3XPoKYhJv}?WxU^Xy@WZw8z~dag zp>ZuSCsC;jqd#E(Ja?YM$P6$8%)l*Sz;9ojm0L1jo+dNE4BQ+8v_B|R!q8*m&~6>r z(C;Jlw+Ttmrn>~8@)&w-9AXcOaH)tcRro80aOvn*J}&gwICSYC?8sQhk1YHRMcC2N zuWUGo&>^?X05fovfhG4C(D}dh_xu0VBz|HBn1TO_0ny%!HV61h{%l=%IXY_<>Jcgl p#WfCpr=VevVvMDucoo$O`V|?7p~uD{dQkXBz|z1CGjO8}`~pc)WL5wG From c45dfe06899a1d463c480baa898a4a26e2de46f9 Mon Sep 17 00:00:00 2001 From: Pete Inge Date: Sat, 21 Jun 2025 11:10:43 -0400 Subject: [PATCH 03/12] chore: Add taxonopmy dependency for the bc_api_example module --- modules/bc_api_example/bc_api_example.info.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/bc_api_example/bc_api_example.info.yml b/modules/bc_api_example/bc_api_example.info.yml index 41064b5..26f3cb9 100644 --- a/modules/bc_api_example/bc_api_example.info.yml +++ b/modules/bc_api_example/bc_api_example.info.yml @@ -6,6 +6,7 @@ core_version_requirement: ^10 || ^11 package: Bluecadet dependencies: + - drupal:taxonomy - bc_api_base:bc_api_base configure: null From 969550fbe36be64446471e68f067fcb5f970fcb5 Mon Sep 17 00:00:00 2001 From: Pete Inge Date: Sat, 21 Jun 2025 11:12:06 -0400 Subject: [PATCH 04/12] =?UTF-8?q?fix:=20FIx=20the=2040x=20response=20check?= =?UTF-8?q?s=20for=20the=20=E2=80=9C/api=3Fapi-key=3D123456=E2=80=9D=20uri?= =?UTF-8?q?=20type=20to=20properly=20error=20out=20with=20a=20json=20respo?= =?UTF-8?q?nse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/EventSubscriber/ApiSubscriber.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/EventSubscriber/ApiSubscriber.php b/src/EventSubscriber/ApiSubscriber.php index 361b558..e377b1a 100644 --- a/src/EventSubscriber/ApiSubscriber.php +++ b/src/EventSubscriber/ApiSubscriber.php @@ -138,9 +138,10 @@ public function onException($event) { public function on400(RequestEvent $event) { $request = $event->getRequest(); + $path = $request->getPathInfo(); $exception = $event->getThrowable(); - if (strpos($request->getRequestUri(), "/api/") === 0 || $request->getRequestUri() == "/api") { + if (strpos($request->getRequestUri(), "/api/") === 0 || $path == "/api") { $data = [ 'status' => (int) $exception->getStatusCode(), 'error_msg' => 'Bad Request', @@ -163,9 +164,10 @@ public function on400(RequestEvent $event) { public function on403(RequestEvent $event) { $request = $event->getRequest(); + $path = $request->getPathInfo(); $exception = $event->getThrowable(); - if (strpos($request->getRequestUri(), "/api/") === 0 || $request->getRequestUri() == "/api") { + if (strpos($request->getRequestUri(), "/api/") === 0 || $path == "/api") { $data = [ 'status' => (int) $exception->getStatusCode(), @@ -189,9 +191,10 @@ public function on403(RequestEvent $event) { public function on404(RequestEvent $event) { $request = $event->getRequest(); + $path = $request->getPathInfo(); $exception = $event->getThrowable(); - if (strpos($request->getRequestUri(), "/api/") === 0 || $request->getRequestUri() == "/api") { + if (strpos($request->getRequestUri(), "/api/") === 0 || $path == "/api") { $data = [ 'status' => (int) $exception->getStatusCode(), 'error_msg' => 'Not Found', @@ -201,7 +204,7 @@ public function on404(RequestEvent $event) { $event->setResponse($response); // Log this call. - $this->loggerFactory->get('bc_api')->error("403: Bad Api call. " . $exception->getMessage(), ["request" => $request, "exception" => $exception]); + $this->loggerFactory->get('bc_api')->error("404: Bad Api call. " . $exception->getMessage(), ["request" => $request, "exception" => $exception]); } } From 1bb116b6753099b8d3c9bc052f2831e330da910c Mon Sep 17 00:00:00 2001 From: Pete Inge Date: Sat, 21 Jun 2025 11:13:42 -0400 Subject: [PATCH 05/12] fix: Updates for typehinting and upgrades --- .../src/Controller/ApiControllerPirateExample.php | 1 + src/Controller/ApiControllerBase.php | 4 ++-- tests/src/Functional/ResponseTests.php | 4 ++-- tests/src/Unit/QueryValidationTests.php | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/bc_api_example/src/Controller/ApiControllerPirateExample.php b/modules/bc_api_example/src/Controller/ApiControllerPirateExample.php index 81ced00..f778d9a 100644 --- a/modules/bc_api_example/src/Controller/ApiControllerPirateExample.php +++ b/modules/bc_api_example/src/Controller/ApiControllerPirateExample.php @@ -93,6 +93,7 @@ public function getResourceQueryResult() { public function getResourceListQueryResult() { // This method should be overridden for any endpoint. $query = $this->entityTypeManager->getStorage('node')->getQuery(); + $query->accessCheck(TRUE); $query->condition('status', $this->privateParams['status']); $query->condition('type', 'pirate'); diff --git a/src/Controller/ApiControllerBase.php b/src/Controller/ApiControllerBase.php index 28264ef..e86e370 100644 --- a/src/Controller/ApiControllerBase.php +++ b/src/Controller/ApiControllerBase.php @@ -174,14 +174,14 @@ class ApiControllerBase extends ControllerBase implements ApiControllerInterface /** * Limit the number of results. * - * @var string + * @var int */ protected $prev = ""; /** * Limit the number of results. * - * @var string + * @var int */ protected $next = ""; diff --git a/tests/src/Functional/ResponseTests.php b/tests/src/Functional/ResponseTests.php index 404f5b2..e83ffcc 100644 --- a/tests/src/Functional/ResponseTests.php +++ b/tests/src/Functional/ResponseTests.php @@ -30,7 +30,7 @@ class ResponseTests extends BrowserTestBase { */ protected $keyAuthConfig; - protected $defaultTheme = 'classy'; + protected $defaultTheme = 'stable9'; protected $dumpHeaders = TRUE; @@ -55,7 +55,7 @@ class ResponseTests extends BrowserTestBase { /** * {@inheritdoc} */ - protected function setUp() { + protected function setUp(): void { parent::setUp(); $this->keyAuth = $this->container->get('key_auth'); diff --git a/tests/src/Unit/QueryValidationTests.php b/tests/src/Unit/QueryValidationTests.php index 4cd35cf..a85e058 100644 --- a/tests/src/Unit/QueryValidationTests.php +++ b/tests/src/Unit/QueryValidationTests.php @@ -20,7 +20,7 @@ class QueryValidationTests extends UnitTestCase { /** * {@inheritdoc} */ - public function setUp() { + protected function setUp(): void { // Nothing to do here. parent::setUp(); } From 71eea13b59bd9eafab518030c16a638e915dc84d Mon Sep 17 00:00:00 2001 From: Pete Inge Date: Sat, 21 Jun 2025 11:24:29 -0400 Subject: [PATCH 06/12] feat: setup new methods in the base controller to allow for new trait to create new types of Reponses --- src/Controller/ApiControllerBase.php | 22 ++++++++++++++++------ src/Controller/ApiControllerInterface.php | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/Controller/ApiControllerBase.php b/src/Controller/ApiControllerBase.php index e86e370..41f44ff 100644 --- a/src/Controller/ApiControllerBase.php +++ b/src/Controller/ApiControllerBase.php @@ -464,9 +464,8 @@ final public function getResource(Request $request) { return []; } - $response = new JsonResponse($this->return_data); - // Alter it. - $this->responseAlter($response); + // $response = new JsonResponse($this->return_data); + $response = $this->createResponse(); return $response; } @@ -533,9 +532,7 @@ final public function getResourceList(Request $request) { return []; } - $response = new JsonResponse($this->return_data); - // Alter it. - $this->responseAlter($response); + $response = $this->createResponse(); return $response; } @@ -589,6 +586,19 @@ public function buildLinks() { $this->next = ""; } + /** + * {@inheritdoc} + */ + public function createResponse(): Response { + $response = new JsonResponse($this->return_data); + $response->setStatusCode($this->return_data['status']); + + // Allow subclasses to alter it. + $this->responseAlter($response); + + return $response; + } + /** * {@inheritdoc} */ diff --git a/src/Controller/ApiControllerInterface.php b/src/Controller/ApiControllerInterface.php index 211cc9c..476cf5f 100644 --- a/src/Controller/ApiControllerInterface.php +++ b/src/Controller/ApiControllerInterface.php @@ -74,8 +74,23 @@ public function getResource(Request $request); */ public function getResourceList(Request $request); + /** + * Create the response object. + * + * We allow subclasses to override this method if they need to create + * different types of responses, however, they can also alter the response + * later, if they only need minor changes. + * + * @return Symfony\Component\HttpFoundation\Response + * The response object. + */ + public function createResponse(): Response; + /** * Alter the response object before executing. + * + * @param \Symfony\Component\HttpFoundation\Response $response + * The response object to alter. */ public function responseAlter(Response $response); From c2ae0817c2c708f3db6ac0e4435bf746d7037601 Mon Sep 17 00:00:00 2001 From: Pete Inge Date: Sat, 21 Jun 2025 11:25:01 -0400 Subject: [PATCH 07/12] feat: Create to Trait for a CacheableJsonResponse --- src/CacheableJsonResponseTrait.php | 80 ++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/CacheableJsonResponseTrait.php diff --git a/src/CacheableJsonResponseTrait.php b/src/CacheableJsonResponseTrait.php new file mode 100644 index 0000000..8acb887 --- /dev/null +++ b/src/CacheableJsonResponseTrait.php @@ -0,0 +1,80 @@ +cacheMetadata)) { + $this->cacheMetadata = new CacheableMetadata(); + } + return $this->cacheMetadata; + } + + /** + * Adds a dependency on an object: merges its cacheability metadata. + * + * For instance, when a response depends on some configuration, an entity, or + * an access result, we must make sure their cacheability metadata is present + * on the response. This method makes doing that simple. + * + * @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $dependency + * The dependency. If the object implements CacheableDependencyInterface, + * then its cacheability metadata will be used. Otherwise, the passed in + * object must be assumed to be uncacheable, so max-age 0 is set. + * + * @return $this + */ + public function addCacheableDependency($dependency) { + + $this->cacheMetadata = $this->getCacheableMetadata()->merge(CacheableMetadata::createFromObject($dependency)); + + return $this; + } + + /** + * {@inheritdoc} + * + * Override the base classes response, to create a cacheable response. + */ + public function createResponse(): Response { + $response = new CacheableJsonResponse($this->return_data); + + // Attach cache metadata if available. + if ($cache_metadata = $this->getCacheableMetadata()) { + $response->addCacheableDependency($cache_metadata); + } + + // Alter it. + $this->responseAlter($response); + + return $response; + } + +} From efe7e4c80fefdcf2d252ceeefd8975b6156ccb1f Mon Sep 17 00:00:00 2001 From: Pete Inge Date: Sat, 21 Jun 2025 11:30:21 -0400 Subject: [PATCH 08/12] feat: add in examples for CacheableJsonResponseTrait in bc_api_example module --- .../bc_api_example/bc_api_example.routing.yml | 23 +++ ...ntrollerCacheableResponsePirateExample.php | 173 ++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 modules/bc_api_example/src/Controller/ApiControllerCacheableResponsePirateExample.php diff --git a/modules/bc_api_example/bc_api_example.routing.yml b/modules/bc_api_example/bc_api_example.routing.yml index bf7b0e5..f9efc64 100644 --- a/modules/bc_api_example/bc_api_example.routing.yml +++ b/modules/bc_api_example/bc_api_example.routing.yml @@ -20,3 +20,26 @@ bc_api_example.pirates_detail: parameters: nid: type: entity:node + +bc_api_example.cacheable.pirates: + path: '/api/cacheable/pirates' + methods: [GET, HEAD] + defaults: + _controller: '\Drupal\bc_api_example\Controller\ApiControllerCacheableResponsePirateExample:getResourceList' + requirements: + _permission: 'use api' + options: + _auth: [ 'key_auth' ] + +bc_api_example.cacheable.pirates_detail: + path: '/api/cacheable/pirates/{nid}' + methods: [GET, HEAD] + defaults: + _controller: '\Drupal\bc_api_example\Controller\ApiControllerCacheableResponsePirateExample:getResource' + requirements: + _permission: 'use api' + options: + _auth: [ 'key_auth' ] + parameters: + nid: + type: entity:node diff --git a/modules/bc_api_example/src/Controller/ApiControllerCacheableResponsePirateExample.php b/modules/bc_api_example/src/Controller/ApiControllerCacheableResponsePirateExample.php new file mode 100644 index 0000000..6278cb6 --- /dev/null +++ b/modules/bc_api_example/src/Controller/ApiControllerCacheableResponsePirateExample.php @@ -0,0 +1,173 @@ +params)) { + $cid .= ":" . implode(":", $this->params); + } + + $cid .= ":page-" . $this->page; + $cid .= ":limit-" . $this->limit; + + return $cid; + } + + /** + * {@inheritdoc} + */ + public function initCacheTags() { + $this->cacheTags = [ + 'myAwesomeCoolCacheTag', + ]; + } + + /** + * {@inheritdoc} + */ + public function setParams() { + // It would be good to always run this. + parent::setParams(); + + $this->privateParams['status'] = 1; + + // Here we also want to handle any bad param errors... + } + + /** + * {@inheritdoc} + */ + public function getDefaultPlatform() { + return "cinder"; + } + + /** + * {@inheritdoc} + */ + public function getResourceQueryResult() { + // Just having this here as an example. + // Most times no need to override this method. + parent::getResourceQueryResult(); + + if (isset($this->resource) && !empty($this->resource)) { + $this->addCacheableDependency($this->resource); + } + } + + /** + * {@inheritdoc} + */ + public function getResourceListQueryResult() { + // This method should be overridden for any endpoint. + $query = $this->entityTypeManager->getStorage('node')->getQuery(); + $query->accessCheck(TRUE); + $query->condition('status', $this->privateParams['status']); + $query->condition('type', 'pirate'); + + $count_query = clone $query; + + $query->range(($this->page * $this->limit), $this->limit); + $entity_ids = $query->execute(); + + // Must set total result count so we can properly page. + $this->resultTotal = (int) $count_query->count()->execute(); + + // Process Items. + $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($entity_ids); + + $this->rawData = $nodes; + } + + /** + * {@inheritdoc} + */ + public function buildAllResourceData() { + $data = []; + + foreach ($this->rawData as $node) { + $this->cacheTags = array_merge($this->cacheTags, $node->getCacheTags()); + $this->addCacheableDependency($node); + $created_changed = $this->transformer->createdChangedFieldVals($node); + + $item = [ + 'nid' => (int) $node->id(), + 'cms_title' => $node->label(), + 'created' => $created_changed[0], + 'updated' => $created_changed[1], + ]; + $data[] = $item; + } + + $this->data = $data; + } + + /** + * {@inheritdoc} + */ + public function buildLinks() { + // This method should be overridden for any endpoint. + $base_url = $this->request->getSchemeAndHttpHost() . $this->request->getPathInfo(); + $tmp_query_params = $this->params; + $tmp_query_params['platform'] = $this->platform; + $tmp_query_params['limit'] = $this->limit; + + if ($this->page == 0) { + $this->prev = ""; + } + else { + $tmp_query_params['page'] = $this->page - 1; + $this->prev = $base_url . "?" . http_build_query($tmp_query_params); + } + + if ($this->resultTotal > (($this->page + 1) * $this->limit)) { + $tmp_query_params['page'] = $this->page + 1; + $this->next = $base_url . "?" . http_build_query($tmp_query_params); + } + else { + + $this->next = ""; + } + + } + +} From 5497ede1351ee7425e55fd1a8d5cf55e54948b7c Mon Sep 17 00:00:00 2001 From: Pete Inge Date: Sat, 21 Jun 2025 11:30:51 -0400 Subject: [PATCH 09/12] test: create and update tests accodingly --- tests/src/Functional/ResponseTests.php | 44 +++++++++- .../Unit/CacheableJsonResponseTraitTest.php | 87 +++++++++++++++++++ 2 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 tests/src/Unit/CacheableJsonResponseTraitTest.php diff --git a/tests/src/Functional/ResponseTests.php b/tests/src/Functional/ResponseTests.php index e83ffcc..cb89dca 100644 --- a/tests/src/Functional/ResponseTests.php +++ b/tests/src/Functional/ResponseTests.php @@ -88,7 +88,7 @@ public function testHttpResponses() { $user = $this->drupalCreateUser(['use api', 'use key authentication']); // Log in. - $this->drupalLogin($user); + // $this->drupalLogin($user); // Call with Query param. $this->drupalGet('api/pirates', [ @@ -112,7 +112,7 @@ public function testResponseData() { $user = $this->drupalCreateUser(['use key authentication', 'use api']); // Log in. - $this->drupalLogin($user); + // $this->drupalLogin($user); // Call with Query param. $data = $this->drupalGet('api/pirates', [ @@ -128,7 +128,7 @@ public function testResponseData() { $this->drupalGet('api/pirates', [], ['api-key' => $user->api_key->value]); $this->assertSession()->statusCodeEquals(200); - Drupal::moduleHandler()->loadInclude('bc_api_example', 'inc', 'bc_api_example.data'); + \Drupal::moduleHandler()->loadInclude('bc_api_example', 'inc', 'bc_api_example.data'); // Check we have a proper result count. $pirates = bc_api_example_get_pirates_data(); @@ -139,4 +139,42 @@ public function testResponseData() { } + /** + * Test the cacheable pirates API endpoint. + */ + public function testCacheablePiratesApiResponse() { + // Create API user. + $user = $this->drupalCreateUser(['use api', 'use key authentication']); + $this->drupalLogin($user); + + // Call the endpoint with the API key as a query param. + $this->drupalGet('api/cachable/pirates', [ + 'query' => [ + 'api-key' => $user->api_key->value, + ], + ]); + $this->assertSession()->statusCodeEquals(200); + + // Check for JSON content type header. + $headers = $this->getSession()->getResponseHeaders(); + + $this->assertArrayHasKey('Content-Type', $headers); + $this->assertStringContainsString('application/json', $headers['Content-Type'][0]); + + // Check for cache headers. + $this->assertArrayHasKey('X-Drupal-Cache-Tags', $headers, 'Cache tags header is present'); + $this->assertArrayHasKey('Cache-Control', $headers, 'Cache-Control header is present'); + + // Check the response body. + $data = json_decode($this->getSession()->getPage()->getContent(), TRUE); + $this->assertIsArray($data['data']); + $this->assertArrayHasKey('resultTotal', $data); + $this->assertArrayHasKey('data', $data); + + // Check that at least one pirate has a cms_title. + if (!empty($data['data'])) { + $this->assertArrayHasKey('cms_title', $data['data'][0]); + } + } + } diff --git a/tests/src/Unit/CacheableJsonResponseTraitTest.php b/tests/src/Unit/CacheableJsonResponseTraitTest.php new file mode 100644 index 0000000..f304096 --- /dev/null +++ b/tests/src/Unit/CacheableJsonResponseTraitTest.php @@ -0,0 +1,87 @@ + 'bar']) { + return new class($return_data) extends ApiControllerBase { + use CacheableJsonResponseTrait; + + public function __construct($return_data) { + $this->return_data = $return_data; + } + + public function setCacheTags(array $tags) { + $this->getCacheableMetadata()->setCacheTags($tags); + } + + /** + * Public wrapper for testing the protected method. + */ + public function getCacheableMetadataPublic() { + return $this->getCacheableMetadata(); + } + + /** + * Public wrapper for the protected cacheableJsonResponse method. + */ + public function cacheableJsonResponsePublic(Response $response) { + return $this->cacheableJsonResponse($response); + } + + }; + } + + /** + * Tests that getCacheableMetadata returns an instance of CacheableMetadata. + */ + public function testGetCacheableMetadataReturnsObject() { + $controller = $this->getTestController(); + $metadata = $controller->getCacheableMetadataPublic(); + $this->assertInstanceOf(CacheableMetadata::class, $metadata); + } + + /** + * Tests that addCacheableDependency merges cacheable metadata. + */ + public function testSetAndGetCacheTags() { + $controller = $this->getTestController(); + $tags = ['foo:1', 'bar:2']; + $controller->setCacheTags($tags); + $metadata = $controller->getCacheableMetadataPublic(); + $this->assertEquals($tags, $metadata->getCacheTags()); + } + + /** + * Tests that cacheableJsonResponse returns a CacheableJsonResponse. + */ + public function testCreateResponseReturnsCacheableJsonResponse() { + $controller = $this->getTestController(['baz' => 'qux']); + $controller->setCacheTags(['baz:1']); + $response = $controller->createResponse(); + $this->assertInstanceOf(CacheableJsonResponse::class, $response); + $this->assertEquals(json_encode(['baz' => 'qux']), $response->getContent()); + $this->assertEquals(200, $response->getStatusCode()); + // Check cache tags are present. + $metadata = $response->getCacheableMetadata(); + $this->assertContains('baz:1', $metadata->getCacheTags()); + } + +} From 72aee85804fc752f20d27308b2821a41c1adf6f8 Mon Sep 17 00:00:00 2001 From: Pete Inge Date: Sun, 22 Jun 2025 13:49:25 -0400 Subject: [PATCH 10/12] test(fix): Fixing route --- tests/src/Functional/ResponseTests.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/Functional/ResponseTests.php b/tests/src/Functional/ResponseTests.php index cb89dca..04a70d6 100644 --- a/tests/src/Functional/ResponseTests.php +++ b/tests/src/Functional/ResponseTests.php @@ -148,7 +148,7 @@ public function testCacheablePiratesApiResponse() { $this->drupalLogin($user); // Call the endpoint with the API key as a query param. - $this->drupalGet('api/cachable/pirates', [ + $this->drupalGet('api/cacheable/pirates', [ 'query' => [ 'api-key' => $user->api_key->value, ], From 9092a433194d78cc4fb9ff62e8dbeaa4c374f7e5 Mon Sep 17 00:00:00 2001 From: Pete Inge Date: Tue, 8 Jul 2025 09:39:09 -0400 Subject: [PATCH 11/12] fix: Update cache key examples and comments --- .../Controller/ApiControllerCacheableResponsePirateExample.php | 3 ++- .../src/Controller/ApiControllerPirateExample.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/bc_api_example/src/Controller/ApiControllerCacheableResponsePirateExample.php b/modules/bc_api_example/src/Controller/ApiControllerCacheableResponsePirateExample.php index 6278cb6..c69edd6 100644 --- a/modules/bc_api_example/src/Controller/ApiControllerCacheableResponsePirateExample.php +++ b/modules/bc_api_example/src/Controller/ApiControllerCacheableResponsePirateExample.php @@ -41,7 +41,8 @@ public function getApiCacheTime($id = NULL) { * {@inheritdoc} */ public function getCacheId() { - $cid = "SOMETHING_UNIQUE"; + // This should be a unique string, characters only. + $cid = "yo-ho-ho"; if (!empty($this->params)) { $cid .= ":" . implode(":", $this->params); diff --git a/modules/bc_api_example/src/Controller/ApiControllerPirateExample.php b/modules/bc_api_example/src/Controller/ApiControllerPirateExample.php index f778d9a..aaedb20 100644 --- a/modules/bc_api_example/src/Controller/ApiControllerPirateExample.php +++ b/modules/bc_api_example/src/Controller/ApiControllerPirateExample.php @@ -38,7 +38,8 @@ public function getApiCacheTime($id = NULL) { * {@inheritdoc} */ public function getCacheId() { - $cid = "SOMETHING_UNIQUE"; + // This should be a unique string, characters only. + $cid = "pirates"; if (!empty($this->params)) { $cid .= ":" . implode(":", $this->params); From aec50408e4242b84290569278474d435d301158a Mon Sep 17 00:00:00 2001 From: Pete Inge Date: Tue, 8 Jul 2025 09:39:36 -0400 Subject: [PATCH 12/12] refactor: move some code out of example to the trait itself --- .../ApiControllerCacheableResponsePirateExample.php | 4 ---- src/CacheableJsonResponseTrait.php | 12 ++++++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/modules/bc_api_example/src/Controller/ApiControllerCacheableResponsePirateExample.php b/modules/bc_api_example/src/Controller/ApiControllerCacheableResponsePirateExample.php index c69edd6..1d8a1ea 100644 --- a/modules/bc_api_example/src/Controller/ApiControllerCacheableResponsePirateExample.php +++ b/modules/bc_api_example/src/Controller/ApiControllerCacheableResponsePirateExample.php @@ -89,10 +89,6 @@ public function getResourceQueryResult() { // Just having this here as an example. // Most times no need to override this method. parent::getResourceQueryResult(); - - if (isset($this->resource) && !empty($this->resource)) { - $this->addCacheableDependency($this->resource); - } } /** diff --git a/src/CacheableJsonResponseTrait.php b/src/CacheableJsonResponseTrait.php index 8acb887..f4d8f42 100644 --- a/src/CacheableJsonResponseTrait.php +++ b/src/CacheableJsonResponseTrait.php @@ -58,6 +58,18 @@ public function addCacheableDependency($dependency) { return $this; } + /** + * {@inheritdoc} + */ + public function getResourceQueryResult() { + parent::getResourceQueryResult(); + + // If the resource is set, add it as a cacheable dependency. + if (isset($this->resource) && !empty($this->resource)) { + $this->addCacheableDependency($this->resource); + } + } + /** * {@inheritdoc} *