What it does
Core lets one single-directory component replace another with replaces:, but requires both to declare compatible prop schemas. An override that only changes Twig or CSS therefore has to duplicate the entire props block from the component it replaces and keep that copy in sync.
This module merges the replaced component's schema into the replacement during discovery. If an override declares no props, it inherits the full parent schema, including the required list. If it declares a partial schema, only the missing parts are inherited and any locally defined values take precedence.
How it works
A service provider calls setClass() on the existing plugin.manager.sdc definition, so core continues to own the service arguments. The subclass overrides one protected method, alterDefinitions(), merges parent schemas into definitions using replaces:, then calls parent::alterDefinitions(). Core's definition validation, replacement compatibility checks, and module-weight sorting still run on the merged definitions. The module does not replace or duplicate any of that logic.
Only props is inherited. libraryOverrides is intentionally excluded because relative asset paths resolve against the directory of the component that declares them.
Security surface
The module has no routes, forms, permissions, configuration, database access, hooks, or output of its own. It reads plugin definitions Drupal has already discovered from disk, merges the schemas, and returns the updated definitions. No user-submitted data is involved at any point.
It only makes a schema more permissive, never stricter, and core's SchemaCompatibilityChecker still validates the merged definitions. It cannot be used to bypass core's compatibility checks.
Testing
tests/src/Kernel/PropInheritanceTest.php covers the service swap, full-schema inheritance, partial inheritance including the inherited required list, and verifies that components without replaces: are left unchanged.
GitLab CI is enabled and the latest pipeline passes all ten jobs, including phpunit and phpstan against Drupal 10.6: https://git.drupalcode.org/project/sdc_prop_inherit/-/pipelines/901644
To verify manually, add a component with a props schema to any theme, then create a second component whose .component.yml contains only $schema, name, and replaces: pointing to the first, along with a Twig file. Without this module, core throws an InvalidComponentException. With the module enabled, the replacement component inherits the parent props.
Similar projects
I could not find another contributed project that addresses schema duplication in SDC component replacement. Core issue #3527170 covers related functionality. This module provides a contrib solution for current releases, and if core adds similar functionality in the future, I would expect to deprecate it.
Comments
Comment #2
vishal.kadamComment #3
avpadernoThank you for applying!
Before giving links helpful to understand how the review process works, what to expect from a review, and what to do to avoid a review takes more time than needed, I would like to thank all the reviewers for the work they do.
These applications are volunters-driven, which also means it is not possible to predict when an application will be marked fixed and the applicant will get the permission to opt projects into security advisory policy. While we aim to make an application as quick as possible, it is also important for us that more people review the project used for an application. In this way, we make sure applications do not miss some important points that should be instead reported.
Applications are not meant to be complete debugging sessions that eliminate every existing bug, though. I apologize if sometimes applications seem to go into too-detailed reviews.
Please read Review process for security advisory coverage: What to expect for more details and Security advisory coverage application checklist to understand what reviewers look for. Tips for ensuring a smooth review gives some hints for a smoother review.
The important notes are the following.
Keep in mind that once the project is opted into security advisory coverage, only Security Team members may change coverage.
To the reviewers
Please read How to review security advisory coverage applications, Application workflow, What to cover in an application review, and Tools to use for reviews.
The important notes are the following.
For new reviewers, I would also suggest to first read In which way the issue queue for coverage applications is different from other project queues.
Comment #4
avpadernoRemember to change status, when the project is ready to be reviewed. In this queue, projects are only reviewed when the status is Needs review.
Comment #5
aaronchristian commentedComment #6
hsjbrianwillows commentedReviewed
1.0.xat commit4d9fc2f. Small, focused module, and a real problem worth solving. Both automated checks are clean: PHPCS (Drupal, DrupalPractice) across 10 files gives 0 errors and 0 warnings, and PHPStan at level 5 reports nothing.Findings
1. Chained replacement is order dependent
In
InheritedComponentPluginManager::alterDefinitions():foreach ($definitions as $id => $definition) { $definitions[$id] = $this->inheritProps($definition, $definitions); }The loop iterates a snapshot, but the second argument passes the live array, which is being rewritten as the loop runs. So when
inheritProps()looks up$definitions[$parent_id]['props'], it may see the parent either before or after that parent has itself inherited, depending purely on discovery order.For a single level of replacement this never shows. For a chain, where A replaces B and B in turn replaces C, whether A ends up with C's props depends on whether B happened to be processed first. Discovery order is not something an author controls, and because definitions are cached the outcome gets baked in, so two environments can legitimately disagree.
Worth deciding what chained replacement should mean, then making it deterministic. Resolving parents recursively with a memo, or simply looking parents up in an untouched copy of the original array, both give a stable answer. A test with a three component chain would pin whichever you choose.
2. The "required" merge semantics deserve a line in the README
$definition['props'] += $parent_props;fills in top level keywords the override omits, which includesrequired. That produces two behaviours that are defensible but not obvious:propertiesbut norequiredsilently inherits the parent's entirerequiredlist.requiredreplaces the parent's outright rather than merging, so it can drop a prop the parent required.Core's compatibility check will reject the genuinely broken cases, so this is not a correctness problem. It is a documentation one, and the README is the natural place for it since the whole point of the module is that authors stop thinking about the parent schema.
3. Test coverage gap
The four kernel tests cover the manager swap, the no props case, the partial case and the untouched case, which is a good spread for a module this size. The test module has two replacement components and neither is itself replaced, so the chain case in finding 1 is untested. That is the case I would add first.
4. Small accuracy point in the docblock
The class docblock says the merge runs first so that "core still performs its own definition validation, replacement compatibility check and sorting". The compatibility check and the sorting do both run unconditionally, so the substance is right. Definition validation is the exception: core wraps
isValidDefinition()in anassert(), so it runs in development and CI but not in production. Worth rewording slightly, because the current phrasing implies more of a runtime safety net than production actually has.Things I checked and found correct
setClass()and leaves core owning the arguments, so a constructor change inComponentPluginManagerwill not break the module. It also guards withhasDefinition(). This is the detail most modules of this kind get wrong, and the docblock explains why it was done that way.parent::alterDefinitions(), and core's replacement compatibility check runs at the end of that parent call against the merged result, so an override cannot use this module to smuggle through an incompatible schema. Confirmed inComponentPluginManager::alterDefinitions().+=is the correct operator here. Union keeps the override's own declarations and only fills gaps, which is exactly what the docblock claims._inheriteddebug key is safe. I half expected this to fail validation, but the SDC metadata schema does not setadditionalProperties: falseat the root, so an extra top level key is permitted.OPT_IN_TEST_PREVIOUS_MAJORwith_AUTORUN_PREVIOUS_MAJORset, so the declared^10.6support is actually exercised rather than merely claimed. Nice to see, and the comment explaining why both variables are needed is a kindness to the next maintainer.Summary
Clean, well scoped and unusually well commented for its size, and the extension point it uses is the upgrade safe one. Finding 1 is the only thing I would want resolved before coverage, and it is a small change plus a test. Findings 2 to 4 are documentation and polish.
I use Assisted AI but I review what it generates
Comment #7
aaronchristian commentedThanks @hsjbrianwillows for the thorough review. I really appreciate you digging into the discovery order issue and validating the replacement logic. I made some fixes, please see below:
1. Chained replacements were not always handled correctly.
Updated the replacement handling so chained replacements now resolve consistently, regardless of the order components are discovered. I also added tests covering both the normal case and the edge case where replacements are processed before their targets.
2. Required merge behavior.
Added README documentation explaining how the
requiredlist is handled. Overrides that don't define their own list inherit the parent requirements, while overrides that provide their own list replace the parent's values. Also documented how chained replacements behave.3. Test coverage.
The test suite now includes six kernel tests, including coverage for chained replacement scenarios.
4. Docblock.
Updated the class documentation to better explain when validation runs and how the replacement process works.
Comment #8
aaronchristian commentedComment #9
aaronchristian commentedChanges are on
1.0.x, commit 578d331. Setting this back to Needs review.Pipeline is passing as well: https://git.drupalcode.org/project/sdc_prop_inherit/-/pipelines/912799