Skip to content

SilverStripe 6 compatibility, dev infra, and PHPUnit suite#16

Open
erikfrerejean wants to merge 14 commits into
6from
feature/ss6-compatibility-and-test-integration
Open

SilverStripe 6 compatibility, dev infra, and PHPUnit suite#16
erikfrerejean wants to merge 14 commits into
6from
feature/ss6-compatibility-and-test-integration

Conversation

@erikfrerejean
Copy link
Copy Markdown
Member

Summary

First cut of the 6.0.0-rc.1 release line. Rolls together the SilverStripe 6 / PHP 8.3 baseline, a fresh FrankenPHP-based dev environment, the module's first PHPUnit suite, and supporting CI/static-analysis. See CHANGELOG.md for the full breakdown — highlights below.

Breaking

  • SS6 / PHP 8.3 baseline. silverstripe/framework ^6.0, silverstripe/cms ^6.0, silverstripe/admin ^3.0, unclecheese/display-logic ^4.0, symbiote/silverstripe-gridfieldextensions ^5.0. The previous silverstripe/display-logic ^3 constraint was wrong (the code imports UncleCheese\…) and is corrected here.
  • MenuItem::LinkType is now a backed PHP enum (WeDevelop\Menustructure\Model\LinkType). The LINK_TYPE_* constants are gone; compare against LinkType::Page->value or use LinkType::tryFrom(). DB column moves from Varchar to MySQL Enum('page,url,file,no-link,breakpoint', 'no-link')dev/build migrates in place, existing rows preserved.
  • updateLinkTypes hook + link_types config removed. Valid link types are closed at the PHP level — custom types now require a code change to this module. Trade-off against runtime extensibility for verifiable static analysis.
  • MenuItem::getLink() returns ?string (was string). no-link, breakpoint, unset relations, and empty URLs now return null instead of ''. updateLink extension hook signature changes accordingly.

Added

  • breakpoint link type for structural markers (dividers, mega-menu column breaks) — distinct from the labelled no-link case.
  • First PHPUnit suite (tests/Model/Menu*Test.php) covering slug auto-fill, protected_menus, permission delegation, getLink() across every LinkType branch (with QueryString / AnchorText config flips), LinkingMode, getLevel, and the LastEdited cascade on write/delete.
  • GitHub Actions CI: static-analysis (make analyse + make rector-dry) and a PHPUnit matrix across PHP 8.3 / 8.4 / 8.5 on every push and PR to 6.
  • FrankenPHP dev stack under .docker/, replacing the legacy single-container php-cs-fixer setup. make targets for test, coverage, analyse, rector, rector-dry, flush, dev-build, sh.
  • Rector configuration (make rector / make rector-dry) targeting SS5→SS6 + PHP 8.3 + standard presets — also enforces code style, replacing php-cs-fixer entirely.
  • Restructured docs/usage/, architecture/, contributing.md. CHANGELOG adopts Keep a Changelog format.
  • PHPStan level max with cambis/silverstan, phpstan/phpstan-deprecation-rules, and 100% type coverage.

Internal cleanup

  • Hierarchy cleanup uses $cascade_deletes on both Menu and MenuItem instead of a manual onBeforeDelete loop. Deleting a sub-tree root no longer orphans descendants.
  • MenuItem::$owns removed — neither model is Versioned, so the publish cascade never ran (dead config).
  • All source declares strict_types=1 and uses #[Override] on overridden methods.
  • Permission methods typed mixed $member = null to satisfy 100% type-coverage without narrowing the parent DataObject contract (LSP).

Test plan

  • CI green on 6 target: static-analysis job + PHPUnit matrix (8.3 / 8.4 / 8.5)
  • Local make analyse passes at level max
  • Local make test passes
  • Local make rector-dry reports no pending changes
  • Manual smoke: make up && make dev-build, create a Menu with each LinkType (including breakpoint), confirm CMS field display-logic toggles correctly and dev/build migrates the LinkType column on an existing DB
  • Verify protected_menus config blocks deletion and forces Slug read-only

Replaces the legacy single-container php-cs-fixer-only Docker setup with
the FrankenPHP + MySQL stack pattern used in silverstripe-media-field and
silverstripe-grid. Adds GitHub Actions for code-style, static analysis,
and a PHPUnit matrix; adds dependabot, .gitattributes export-ignore, and
.dockerignore.

Module composer.json is intentionally untouched — SS6 compat and the
PHPUnit suite are followup tasks. The dev env and CI are pinned to PHP
8.2+ because FrankenPHP doesn't publish a PHP 8.1 image.
Establishes the module's first tests on top of the FrankenPHP dev infra,
covering both DataObjects' behavior — slug auto-fill, protected_menus
config gating, permission delegation, getLink across all LinkType
branches (with QueryString/AnchorText config flips), LinkingMode,
getLevel, and LastEdited cascade on write/delete. Adds a `coverage`
make target and declares the package as silverstripe-vendormodule so
SilverStripe's class manifest scans the module's tests/ in test mode.
Bumps PHPStan from level 9 to max and removes
treatPhpDocTypesAsCertain: false (which weakened analysis below max).
Adds cambis/silverstan ^2.1 so PHPStan understands SilverStripe's
config conventions (private static $db / $has_one / $has_many config
properties, has_one method/property typing, Config_ForClass access),
which was the root cause of the bulk of the original 61 errors.

Code fixes:
- Specify HasManyList<MenuItem> generic on @method Items()
- Add @method Menu Menu() on MenuItem (silverstan resolves via has_one)
- Use ::create() instead of new GridFieldConfig_RelationEditor()
- Replace self::config()->prop with self::config()->get('prop')
  (silverstan.configurationProperty.unresolvableType / .unsafe)
- Guard $this->LinkedPage()/$this->File() with ->exists() before ->Link()
- Drop dead `?? ''` after exhaustive match in getLink()
- Add ?Member docblocks on canX() (parent in SS5 is untyped, so
  narrowing the typehint in PHP would violate LSP) and pass
  Permission::checkMember($member ?? 0, ...) to satisfy int|Member —
  the framework's `if (!$member)` branch produces identical runtime
  behaviour for 0 and null.
- Type onAfterWrite() return as void
- Add @return array<int, string> on get_template_global_variables()
- Use is_array() guard in IsProtected() so in_array() haystack is typed,
  and switch from $this->Config() (deprecated ViewableData) to
  static::config().
- Module: PHP ^8.3, silverstripe/cms ^6, silverstripe/framework ^6,
  silverstripe/admin ^3, unclecheese/display-logic ^4 (was misnamed
  silverstripe/display-logic ^3 — code imports UncleCheese\…),
  symbiote/silverstripe-gridfieldextensions ^5. Drop branch-alias to
  follow the SilverStripe `6` branch convention.
- Dev infra: Dockerfile defaults to PHP 8.3; dev composer pulls
  recipe-cms ^6, phpunit ^11, cambis/silverstan, phpstan-deprecation-rules,
  type-coverage, wernerkrauss/silverstripe-rector. PHPStan: level max +
  100% type coverage + deprecation rules. CI matrix: PHP 8.3/8.4/8.5,
  triggers on `6`. Drop php-cs-fixer entirely (Rector handles code style).
- Source: declare(strict_types=1) + #[Override] across the module.
  Menu::forTemplate() returns string to match SS6 ModelData; ViewableData
  → ModelData (Rector). dataFieldByName() and Controller::curr() null-
  guarded for SS6's narrower return types. canX permission methods typed
  `mixed $member` to satisfy 100% type coverage without violating LSP
  against DataObject's untyped parent — same pattern as silverstripe-grid.
Promote MenuItem::LinkType to a first-class WeDevelop\Menustructure\Model\LinkType
backed enum and constrain the column with MySQL Enum. Drops the private const
string + $link_types static array + Varchar trio that was only there to support
runtime extension via updateLinkTypes — that hook is now gone.

Adds a new "breakpoint" link type so downstream templates can render structural
markers without the ambiguity of overloading no-link.

BREAKING CHANGE: updateLinkTypes extension and link_types config array removed;
LinkType column changes from Varchar to Enum (dev/build performs an in-place
ALTER TABLE — existing values are preserved).
Replace Menu::onBeforeDelete() manual loop with $cascade_deletes config
and add the same on MenuItem so deleting a sub-tree root no longer
orphans its descendants. Drop MenuItem's $owns = ['File'] — neither
model is Versioned, so the publish cascade never runs and the config
was dead.

Pin the MenuItem cascade with a regression test that deletes the
middle of a 3-level chain and asserts descendants are removed while
parent and siblings stay intact. Rename Menu's existing cascade test
to drop the now-stale onBeforeDelete reference.
PHPDoc test metadata is deprecated in PHPUnit 10+ and removed in
PHPUnit 12; switch to the supported attribute form.
Splits the single docs/configuration.md into a grid-style layout sized for
this module: docs/usage/{templates,configuration}.md, docs/architecture/
data-model.md, and docs/contributing.md. README rewritten as a concise
index with Requirements / Installation / Usage / Documentation pointers.

CHANGELOG.md adopts Keep a Changelog format with a single 6.0.0-rc.1
entry aggregating the SS6 baseline, the LinkType enum BC, the
updateLinkTypes hook removal, the cascade_deletes refactor, the
FrankenPHP dev infra, the PHPStan max bump, and the new test suite.
Pre-RC1 history continues to live as GitHub releases.
Updates project guidance to reflect three changes already on the branch:
LinkType is now a backed PHP enum with a Breakpoint case, hierarchy
cleanup runs through cascade_deletes on both models, and CHANGELOG.md is
actively maintained alongside the new docs/ layout.
Adds export-ignore entries for .claude/, .dockerignore, .vscode/, and
CLAUDE.md so they no longer ship in Packagist tarballs alongside the
runtime sources.
Aligns Menu's permission code construction with MenuItem so both models
build the CMS_ACCESS_ string from the class constant rather than a
hardcoded FQCN literal — refactor-safe and statically verifiable.
BREAKING CHANGE: getLink() return type widens from string to ?string.
no-link, breakpoint, page/file with no relation, and url with empty
Url all now return null instead of ''. The updateLink extension hook
signature becomes updateLink(?string &$link).

Also collapses the duplicated LinkType::Page checks for query-string
and anchor decoration into a single guarded block, and replaces
sprintf with .= for the trivial concat.
Documentation should describe current state — migration history is
preserved in the CHANGELOG and git log.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant