Brian Perry: Extending The Drupal API Client
import RadCallout from '../../../components/rad/RadCallout.astro';
As a result of our Pitch-burgh funding, the current focus of the Drupal API Client is to create a fully featured client for Drupal's JSON:API implementation. Even with that goal, we've focused on making our work extensible for other API formats in the future through the implementation of an ApiClient base class. Functionality that could apply to any API client is added to the base class, while anything specific to JSON:API is added to the JsonApiClient class (which extends ApiClient.)
Recently, we have been working on adding Decoupled Router support to our JSON:API Client. I found this implementation to be a great example of the extensibility of the library, so I wanted elaborate on it in a blog post for those who may want to extend the API Client in the future.
The existing JsonApiClient has the following method to retrieve data for a resource:
await client.getResource('node--article', '3347c400-302d-4f6c-8fcb-3e74beb002c8');Ideally, users of Decoupled Router could also get an identical response by resolving a path:
await client.getResource('/articles/give-it-a-go-and-grow-your-own-herbs');To achieve this, we first needed to provide a way to reliably get data from Decoupled Router.
The Decoupled Router EndpointWith the module enabled, Decoupled Router exposes an endpoint with the following structure:
/router/translate-path?path=<path>
Given a path like /articles/give-it-a-go-and-grow-your-own-herbs the endpoint could provide a response similar to:
{ "resolved": "https://dev-drupal-api-client-poc.pantheonsite.io/en/articles/give-it-a-go-and-grow-your-own-herbs", "isHomePath": false, "entity": { "canonical": "https://dev-drupal-api-client-poc.pantheonsite.io/en/articles/give-it-a-go-and-grow-your-own-herbs", "type": "node", "bundle": "article", "id": "11", "uuid": "3347c400-302d-4f6c-8fcb-3e74beb002c8" }, "label": "Give it a go and grow your own herbs", "jsonapi": { "individual": "https://dev-drupal-api-client-poc.pantheonsite.io/en/jsonapi/node/article/3347c400-302d-4f6c-8fcb-3e74beb002c8", "resourceName": "node--article", "pathPrefix": "jsonapi", "basePath": "/jsonapi", "entryPoint": "https://dev-drupal-api-client-poc.pantheonsite.io/en/jsonapi" }, "meta": { "deprecated": { "jsonapi.pathPrefix": "This property has been deprecated and will be removed in the next version of Decoupled Router. Use basePath instead." } } }While easy to make sense of, this response technically doesn't follow the JSON:API spec, which prevents us from using our existing JSON:API Client without modification. We could write a small amount of custom code in JsonApiClient to fetch and handle data from this endpoint, but this case is exactly what our ApiClient base class is intended for. With a similarly small amount of code we can extend the ApiClient class to add only what is unique to the Decoupled Router endpoint, while getting access to all of the features of the base class at the same time.
So rather than writing code specific to JsonApiClient, we decided to create a new DecoupledRouterClient class that our JsonApiClient could then make use of.
Extending ApiClientFor the sake of example, a simple Decoupled Router client could look like this:
// DecoupledRouterClient.ts import { ApiClient, type ApiClientOptions, type BaseUrl, } from "@drupal-api-client/api-client"; export class DecoupledRouterClient extends ApiClient { constructor(baseUrl: BaseUrl, options?: ApiClientOptions) { super(baseUrl, options); const { apiPrefix } = options || {}; this.apiPrefix = apiPrefix || "router/translate-path"; } async translatePath(path: string) { const apiUrl = `${this.baseUrl}/${this.apiPrefix}?path=${path}`; const response = await this.fetch(apiUrl); return response.json(); } }In our constructor, the only modification we need to make is the default value for the API prefix. While the base class doesn't have a default, Decoupled Router uses router/translate-path. Now when instance of DecoupledRouter is created without this option, it will use the default.
We then define a translatePath method that:
- Takes a path of type string
- Uses the fetch method provided by the base class to make a request to Decoupled Router
- Returns a promise with the provided json data
Using an instance of this class would look something like:
// main.ts import { DecoupledRouterClient } from "./DecoupledRouterClient.ts"; const decoupledRouterClient = new DecoupledRouterClient("https://dev-drupal-api-client-poc.pantheonsite.io"); const translatedPath = await decoupledRouterClient.translatePath( "/articles/give-it-a-go-and-grow-your-own-herbs" );<RadCallout>Check out this code sandbox for a live version of the example above.</RadCallout>
Taking Advantage of Additional ApiClient FeaturesWith this example we already have a functional client, but quite a bit more is possible using the features of the ApiClient class we extended. For example, We can already make authenticated requests using any of the supported authentication methods:
// main.ts import { DecoupledRouterClient } from "./DecoupledRouterClient.ts"; const decoupledRouterClient = new DecoupledRouterClient("https://dev-drupal-api-client-poc.pantheonsite.io", { authentication: { type: "OAuth", credentials: { clientId: "client-id", clientSecret: "client-secret" } }, }); // API requests will now be authenticated const translatedPath = await decoupledRouterClient.translatePath( "/articles/give-it-a-go-and-grow-your-own-herbs" );Our example Decoupled Router client could be updated to take advantage of built in caching, logging, or locale support. For example, the following modification would allow us to make use of the defaultLocale option if our Drupal site supports multiple languages:
// DecoupledRouterClient.ts import { ApiClient, type ApiClientOptions, type BaseUrl, } from "@drupal-api-client/api-client"; export class DecoupledRouterClient extends ApiClient { constructor(baseUrl: BaseUrl, options?: ApiClientOptions) { super(baseUrl, options); const { apiPrefix } = options || {}; this.apiPrefix = apiPrefix || "router/translate-path"; } async translatePath(path: string) { // If it exists, incorporate the default locale // into the apiUrl const apiUrlObject = new URL( `${this.defaultLocale ?? ""}/${this.apiPrefix}?path=${path}`, this.baseUrl, ); const apiUrl = apiUrlObject.toString(); const response = await this.fetch(apiUrl); return response.json(); } }Routing is a common problem, so we've added a fully featured getResourceByPath method to our latest @drupal-api-client/json-api-client release. We've also published the Decoupled Router client as a standalone package for anyone who wants to use it separately.
While the caching functionality of the client can lessen the impact, getResourceByPath still makes multiple API calls for uncached data, which leaves room for improvement. We could optimize this in the future by providing support for the subrequests module. That is yet another client for a type of Drupal API that could use the ApiClient base class as a starting point.
We're closing in on the 1.0 release of @drupal-api-client/json-api-client. If you’re interested in contributing, check out our project page on Drupal.org, and join us in the #api-client channel on Drupal Slack.
PreviousNext: Adding real-time processing to QueueWorker plugins
Projects no longer need to rely on unpredictable processing time frames. The SM project can intercept legacy Drupal @QueueWorker items and insert them into the Symfony Messenger message bus, effectively giving existing core and contrib queue workers jobs real-time processing capabilities.
by daniel.phin / 7 February 2024This post is part 5 in a series about Symfony Messenger.
- Introducing Symfony Messenger integrations with Drupal
- Symfony Messenger’ message and message handlers, and comparison with @QueueWorker
- Real-time: Symfony Messenger’ Consume command and prioritised messages
- Automatic message scheduling and replacing hook_cron
- Adding real-time processing to QueueWorker plugins
- Making Symfony Mailer asynchronous: integration with Symfony Messenger
- Displaying notifications when Symfony Messenger messages are processed
- Future of Symfony Messenger in Drupal
@QueueWorker plugin implementations require no modifications, including the method of dispatch, data payload, or the processItem . The data payload must of course be serialisable. Fortunately, most QueueWorker plugins already comply since their data is serialised and stored to the queue table. As always, avoid adding complex objects like Drupal entities to payloads.
RunnersWith queue interception, the sm command can be solely relied upon. Legacy runners such as Drupal web cron, request termination cron (automated_cron.module), and Drush queue:run will be rendered inoperable since they will no longer have anything to process. Consider decommissioning legacy runners when deploying queue interception.
SetupQueue interception is a part of the primary SM module. Adding a single line in settings.php is the only action required to to enabling this feature:
$settings['queue_default'] = \Drupal\sm\QueueInterceptor\SmLegacyQueueFactory::class;SM module will need to be fully installed before this line is added. Consider wrapping the line in a class_exists(SmLegacyQueueFactory::class) to enable in a single deployment.
Existing per-queue backends
Setup may be more complex if projects are utilising per-queue backends or anything other than the default database backend for queues, such as Redis. In that case, carefully evaluate whether to convert all or specific queues to use Symfony Messenger.
Whether per-queue backends are utilised can be determined by looking for queue_service_ or queue_reliable_service_ prefixed items in settings.php.
Routing@QueueWorker jobs are converted to \Drupal\sm\QueueInterceptor\SmLegacyDrupalQueueItem messages in the backend. Knowing this class name allows you to configure transport routing. If routing for this message is not explicitly configured, it will naturally fall back to the default transport, or execute synchronously if there is no routing configuration.
Running the jobsAs usual, when a transport is configured, all you need to do is run sm messenger:consume to execute the tasks. The worker will either listen or poll for messages, and execute them in a very short amount of time after they are dispatched, in a dedicated thread. More information on the worker can be found in post 3 of this series.
The next post covers how Drupal emails can be dispatched to messages, so the web thread can execute faster.
Tagged Symfony, Symfony MessengerDrupal Core News: DrupalCI and all patch testing will be turned off on July 1, 2024
To continue automated testing, projects must convert to GitLab CI and contributors must switch from patches to merge requests by July 1, 2024.
As of July 2023, contributed projects are fully equipped to adopt GitLab CI. In October 2023 we announced that Gitlab CI testing of Drupal core was already five times faster than the legacy DrupalCI system. In our December 2023 maintainer email we announced that our legacy DrupalCI will be retired as soon as July 2024. Now we are announcing further details.
What's happening? Some DrupalCI features are already turned offProjects without DrupalCI testing configured cannot add it anymore. Direct access to log output and artifacts on DrupalCI is no longer available. Results are still summarized on the Automated Testing tab of those project's pages.
With the exception of Drupal 7, it is not possible to run tests on patches against Drupal core anymore. Even for Drupal 7, patches need to be sent for testing manually.
Changing DrupalCI schedules ends on April 30, 2024After April 30, 2024, DrupalCI schedules can not be added or changed, except to remove DrupalCI testing. Tests will keep running with existing configured schedules until removed.
DrupalCI and all patch testing will be turned off on July 1, 2024No DrupalCI tests will be executed after this date. It will also no longer be possible to run tests on any patches on Drupal.org on any project. Existing test results will be kept for six months.
All results from DrupalCI tests will be removed on January 1, 2025This is the end of DrupalCI data retention. No testing results will be available beyond January 1, 2025.
What to do about it? Most projects can start GitLab CI testing with a provided template .gitlab-ci.ymlUsing GitLab CI for testing may seem intimidating at first but the Drupal Association engineering team and a number of community volunteers have spent a lot of time to make it as easy as possible for you to adopt GitLab CI for testing.
There is a preconfigured .gitlab-ci.yml template that will set up everything you need to test your project. This template has several useful features, including a variables file that will be automatically updated by the Drupal Association, to make sure you’re always testing against the currently supported versions of Drupal.
Most project maintainers won’t have to make any changes to the template, just commit the template to your project and your testing should work right away!
Set up GitLab CI for your drupal.org project today.
Contributors must use merge requests on all projects to get automated testing after July 1, 2024GitLab CI, like almost all modern CI systems, is designed to test merge requests. If you have ever contributed to a project on GitHub, you pretty much already know how it works. The advantage of Drupal.org merge requests is that they are collaborative by default, so you and fellow contributors can work in the same fork.
Read the best practices for contributing through merge requests.
TechStage | Top 10: Die besten Smartphones bis 300 Euro – Motorola und Xiaomi vor Samsung
Mehr als ein Einsteiger-Smartphone sollte das neue Mobilgerät schon können, aber gleichzeitig nicht viel kosten? TechStage zeigt die besten Handys bis 300 Euro.
ImageX: Drupal Calendar Creation Unleashed: Useful Modules And A Step-by-Step Walkthrough
Authored by: Nadiia Nykolaichuk.
One of the earliest known calendars was created by ancient Egyptians, who used hieroglyphics and carvings to represent the months, days, and important events. Today, visually appealing and user-friendly calendars are easily created on websites, all thanks to powerful CMSs like Drupal. We’ll share some modules in Drupal that are available for calendar creation and management, and carefully walk you through the key steps of building a calendar.
TechStage | Priwatt Pribasic Duo im Test: Balkonkraftwerk mit Top-Panels und 860 Wp
Das Balkonkraftwerk Pribasic Duo von Priwatt erzeugt dank bifazialer Solarpanels bis 860 Wp und kann 800 Watt ins heimische Stromnetz abgeben. Wie sich die zweiseitigen Photovoltaikmodule in der Praxis schlagen, zeigt unser Test.
DrupalEasy: Why you should care about using settings.local.php
Teaching folks why a settings.local.php file is an important part of setting up a personal development environment is so important to us here at DrupalEasy that it is a foundational part of both of our long-form Drupal training courses.
While preparing for an upcoming podcast mini-series I've been invited to participate in with the Talking Drupal folks, I'll be mentoring someone who is looking to re-enter the Drupal development scene after missing out on Drupal's transition from its pre-Symfony days. One of the tasks I have outlined for this person is to set up a settings.local.php file. When I went to find a good resource for the "why" and the "how", I came up empty. I couldn't find a single, up-to-date resource that conveyed what I feel is important.
So, that's what this blog post is all about.
Why use settings.local.php?Using a settings.local.php is all about configuring your local development environment for a project to be as useful to you, the developer, as possible. Default settings in this file do things like force all errors to the screen and disabling some of the (but not all) Drupal caches.
This file also pulls in the sites/development.services.yml file which contains some useful service class parameters and overrides - again, things that are useful for local development.
Setting up a settings.local.php literally takes less than two minutes, and when I see a Drupal developer struggling to figure out a white-screen-of-death error on their local environment, I can't help but ????
In addition to adding Drupal's core-dev dependencies and the Devel module, enabling the settings.local.php file is literally one of the first things I do when setting up a new site.
How do I enable the settings.local.php?I'm so glad you asked.
Step 1Copy sites/example.settings.local.php to sites/default/settings.local.php. Use a user interface (like the MacOS Finder) or the command line - it doesn't matter.
cp web/sites/example.settings.local.php web/sites/default/settings.local.phpStep 2Uncomment the conditional include for settings.local.php in your sites/default/settings.php file - it looks like this:
# if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) { # include $app_root . '/' . $site_path . '/settings.local.php'; # }Uncommented, it looks like this:
if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) { include $app_root . '/' . $site_path . '/settings.local.php'; }Step 3There is no step 3. This blog post is complete.