mark.ie: Show the last author of a node in the Drupal content list
Instead of showing the original author of a node, show the last person to edit it.
Instead of showing the original author of a node, show the last person to edit it.
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.
With 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.
For 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:
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>
With 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.
The Webform module works as a form of builder and submission manager within the Drupal framework, offering a wide range of levels of flexibility and ease for site builders. This tool empowers website creators to efficiently develop a range of forms, with the added benefit of default settings for quick implementation. Delving into its impressive features, the Webform module is known for its user-friendly interface. Users can swiftly create forms using default configurations or take advantage of the module's customization options to tailor forms to precise specifications.
Beyond this, the Webform module boasts a suite of powerful features, making it a core asset of the Drupal ecosystem. But there’s more to it. Let’s have an overview of the most important features and functionalities of the Webform.
In this second part (check the first part!) of our Tag1 Team Talk on Gander, the new Automated Performance Testing Framework integrated into Drupal Core, we get a live demo from Nat Catchpole (aka. Catch), the lead developer on the project. Nat takes us on a tour through this high-impact tool developed by Tag1 in collaboration with the Google Chrome Team, showing you how you can get up and running with automated performance testing for your projects. Gander is poised to significantly impact Drupal's user experience, performance and Core Web Vitals by creating visibility into how Drupal performs on the front and back end. Catch shares his expert insights into the development and application of Gander and shows how easy it is for developers to start extending and using this today on their projects! Whether you're a Drupal developer looking to improve your project's performance or simply curious about the latest in Drupal technology, this episode offers valuable knowledge and practical advice on getting your Drupal website to perform optimally. With discussions on Gander's immediate benefits and future potential in the Drupal community, this episode is a must-watch for anyone interested in taking their projects to the next level...
Read more Mariano Tue, 02/06/2024 - 05:41Projects 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.
This post is part 5 in a series about Symfony Messenger.
@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.
With 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.
Queue 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
.
@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.
As 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.
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.
Projects 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.
After 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.
No 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.
This is the end of DrupalCI data retention. No testing results will be available beyond January 1, 2025.
Using 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.
GitLab 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.
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.
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.
I'm so glad you asked.
Copy 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.php
Uncomment 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';
}
There is no step 3. This blog post is complete.
For years, the last weekend in January has been a special one for the worldwide community behind the CMS system Drupal: The Global Contribution Weekend. Factorial has been a major contributor to the Drupal community for many years, so we hosted our own hybrid Global Contribution Weekend event.
Webform is a Drupal module that allows you to create forms directly in Drupal without using a 3rd party service.
It can be used to create basic “Contact Us” and complex application forms with custom field logic and integration.
In this getting started guide you’ll learn how to:
This tutorial accompanies the video above.