Problem/Motivation
Currently, pattern schemas are loaded and undergo a "dereferencing" process to normalize all references to other schemas using namespace references and allow them to be loaded successfully from an API endpoint. The result of this converts "$ref" values in a schema from a namespaced value like @patternkit/atoms/example/src/example to a local link through the Patternkit API controller like /patternkit/api/patternkit/atoms/example/src/example instead. This allows schema processing to request the schema for the referenced pattern at runtime and populate it in place where the reference is. This is used both in the JSON Editor form as the form is being built, and in the server-side schema traversal validation and traversal logic where the schema has to be fully resolved.
When patterns become heavily interrelated and/or nested with lots of references to other patterns, you encounter issues with a great deal of processing or requesting required to fully resolve the schema. An example of this is described by #3268970: Fetching schemas takes very long time.
If, instead, schema references were resolved server-side when the schema is read in initially, the fully-resolved schema could be loaded in all uses and further requests and loading would not be required. This would also allow for caching of the full schema rather than just the top-level pattern without the contents of lower-level referenced schemas. As it exists now, patterns used in blocks have their schemas cached in Patternkit Pattern entities, but these cached schemas contain the normalized reference links instead of the resolved schema values, so changes to these referenced pattern schemas may result in breaking changes to the uses of this top-level pattern without manual refreshing of the cached data.
Proposed resolution
When a pattern is first parsed and the schema asset is loaded, the Swaggest library could be used to import the schema fully and then serialize the result for caching. This sidesteps the currently implemented complex traversal logic for replacing reference links, and incorporates the fully resolved schema, including sub-schemas, into all uses of the pattern.
This solution was initially implemented as part of the work in #3295824: Library Service Refactor and Cleanup, but as described in #3295824-8: Library Service Refactor and Cleanup patterns resolving with circular references lead to an infinite recursion and eventual failure. As a result, the changes for this feature were removed from that work, but the drafted change record still exists here.
The best place to resolve this may actually be in the underlying Swaggest library, so one of the early steps for this issue should be summing up the issue and reporting it there.
Remaining tasks
- Peer review
- Additional testing for updating on existing sites
Add missing tests for new classesDocument new classes and change recordsBrowser tests covering updating existing pattern blocks
User interface changes
- All patterns containing references are having their loaded schemas updated to use bundled content. This results in existing block placements for them being flagged as needing schema updates when they are next edited.
- Patterns including a lot of references may currently take a long time to load the JSON Editor form as it requires all remote schemas to be loaded. Newly created blocks using these patterns or these blocks after updating the schemas should now load much more quickly.
API changes
See change record for more detail: https://www.drupal.org/node/3303797
New services
patternkit.pattern.dependency_resolver: A new service to identify dependencies in patterns and load them in the most efficient order.patternkit.pattern.repository: A new service for loading pattern entities from their definitions and creating bundled schemas containing all referenced dependencies.patternkit.schema.context_builder: A new service split out from the recently introducedpatternkit.schema.schema_factoryservice. This service is responsible for assembling and configuring Schema operation contexts consistently with necessary default options.patternkit.schema.schema_handler: A utility service for processing and manipulating references discovered within pattern schemas.patternkit.schema.data_preprocessor.factory: A factory service for instantiating schema DataPreProcessor utilities used in schema contexts for a variety of processing tasks.
New Way to Instantiate Patterns
Previously, the most direct path to loading a pattern and getting the schema was to use the \Drupal\patternkit\Entity\Pattern::create() method. The new patternkit.pattern.repository service provides a method to get the pattern entity directly and should be used for this instead:
$pattern = \Drupal::service('patternkit.pattern.repository')->getPattern('@patternkit/atoms/example/src/example');
$schema = $pattern->getSchema();
Loading a bundled schema
If a pattern's schema is needed with all dependencies included, the patternkit.pattern.repository service may also be used for this.
$schema = \Drupal::service('patternkit.pattern.repository')->getBundledPatternSchema('@patternkit/atoms/example/src/example');
$schema_json = json_encode($schema);
Listing all discovered patterns
If only a list of discovered patterns is needed, it is no longer necessary to load and iterate through all discovered pattern definitions. The patternkit.pattern.repository service provides a new method for retrieving a list of only the names of all known patterns:
$patternRepository = \Drupal::service('patternkit.pattern.repository');
foreach ($patternRepository->getAllPatternNames() as $pattern) {
// Do something with each pattern.
}
Data model changes
New pattern entity dependencies field
An unlimited cardinality field on the patternkit_pattern entity populated during pattern discovery with references discovered immediately within a pattern's schema file.
This list of dependencies may be retrieved from an instantiated pattern entity with the new $pattern->getDependencies() method. Note this field is only populated with dependencies referenced directly within the pattern's schema. To identify any further dependencies referenced from those dependencies the new patternkit.pattern.dependency_resolver service may be used.
Testing Instructions
Patch-based workflow
Since this change introduces new Composer dependencies, testing it strictly in a patch-based workflow requires additional steps.
This work is also based off of the current work in the 9.1.x branch which includes work beyond the most recent release (beta4). To account for this and ensure the patch will apply, an existing installation of Patternkit will need to be updated to the latest dev release.
composer require drupal/patternkit:dev-9.1.x
You'll need to add the following to the patches section of your composer.json, but you will also need to manually require the dependencies listed below.
{
...
"extra": {
"patches": {
"drupal/patternkit": {
"#3297124: Flatten Schema References During Schema Loading": "https://git.drupalcode.org/project/patternkit/-/merge_requests/39/diffs.patch"
}
}
}
}
You will also need to manually add these dependencies:
composer require marcj/topsort:\^2.0.0 symfony/polyfill-php80:\^1 symfony/polyfill-php81:\^1 webmozart/assert:\^1.0
Custom Git Repository
Alternatively, you may specify the fork repository directly to install from as shown below.
- The custom package definition must be added above the standard packages.drupal.org composer reference. This ensures the package reference for "drupal/patternkit" is loaded from the fork first.
- You'll need to change the required version of the patternkit module to
dev-3297124-flatten-schema-references. - After making these changes, you may install the fork with the command
composer require drupal/patternkit:dev-3297124-flatten-schema-references.
{
...
"repositories": [
{
"type": "package",
"package": {
"name": "drupal/patternkit",
"type": "drupal-module",
"version": "dev-3297124-flatten-schema-references",
"source": {
"url": "https://git.drupalcode.org/issue/patternkit-3297124.git",
"type": "git",
"reference": "3297124-flatten-schema-references"
},
"require": {
"php": ">=7.4.0",
"ext-json": "*",
"marcj/topsort": "^2.0.0",
"swaggest/json-schema": "^0.12",
"symfony/finder": "^3.4 || ^4.0",
"symfony/polyfill-php80": "^1",
"symfony/polyfill-php81": "^1",
"symfony/yaml": "^3.4 || ^4.0",
"webmozart/assert": "^1.0",
"webmozart/path-util": "^2.1.0"
}
}
},
{
"type": "composer",
"url": "https://packages.drupal.org/8"
},
...
],
...
"require": {
...
"drupal/patternkit": "dev-3297124-flatten-schema-references",
...
},
...
}
Issue fork patternkit-3297124
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
cyb_tachyon commentedAs noted above, both infinite recursion, as well as server-side request load are important considerations.
Some libraries may be highly dependent on third party libraries (as noted in the JSON Schema docs, this is by design) and perhaps a good solution is to allow server-side resolution and cache hydration on a per-library basis with library configuration options, with a default to "on" for all new libraries.
Logging the number of external requests when hydrating the cache, either to the Config GUI or dblog, would be helpful for server admins looking for why their Drupal install is making such requests and driving up costs.
This isn't problematic at this time as very few JSON Schema web libraries I've seen currently use this level of domain interdependence. However, most design proposals that I've seen for modern web component libraries rely entirely on URL-based file loading and caching, so this will be an issue sooner rather than later.
Comment #3
sluceroComment #5
sluceroI've pushed up my work so far on this and opened up a merge request to start getting other eyes and feedback on it. Apologies in advance for the sheer size of this chunk of work, but it proved to be very broad in its implementation and continued to grow in complexity.
Remaining tasks
Comment #6
sluceroI've taken care of several of the remaining tasks I noted previously:
Add missing tests for new classesBrowser tests covering updating existing pattern blocksThis has fixed the tests and they're passing now. The most notable next step that's needed is peer review and testing, but I'll swing back and work on documenting the new classes soon as well.
Remaining tasks
Testing Instructions
Patch-based workflow
Since this change introduces new Composer dependencies, testing it strictly in a patch-based workflow requires additional steps.
This work is also based off of the current work in the
9.1.xbranch which includes work beyond the most recent release (beta4). To account for this and ensure the patch will apply, an existing installation of Patternkit will need to be updated to the latest dev release.You'll need to add the following to the patches section of your composer.json, but you will also need to manually require the dependencies listed below.
You will also need to manually add these dependencies:
Custom Git Repository
Alternatively, you may specify the fork repository directly to install from as shown below.
dev-3297124-flatten-schema-references.composer require drupal/patternkit:dev-3297124-flatten-schema-references.Comment #7
sluceroComment #8
sluceroI've drafted a change record to outline the primary new services that have been introduced and how to use them here: https://www.drupal.org/node/3303797
I've also expanded the issue summary to include documentation about the new changes.
Comment #9
sluceroComment #10
sluceroComment #11
sluceroComment #12
sluceroComment #13
sluceroComment #14
krisahil commentedI reviewed this code and tested it against a site with large number of nested schemas. Even after disabling the client-side schema cache, this change lets the JSON Editor tool load very quickly (with no need to warm the client-side caches).
Thanks, @slucero!
Comment #15
sluceroComment #17
sluceroMerged for inclusion in the Beta 5 release!