Skip to content
This repository was archived by the owner on Nov 9, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Keep development-only files out of dist installs (Packagist zips). CI and local module work must
# therefore install this module as a PATH repository with symlink: true, or a real checkout -
# a dist install contains no tests at all.
/tests export-ignore
/.github export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.editorconfig export-ignore
/.babelrc export-ignore
239 changes: 239 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
name: CI

on:
push:
# The repository's default branch (checked with `gh api repos/restruct/blockbase`)
branches: [main]
pull_request:
workflow_dispatch:

jobs:
test:
name: SS${{ matrix.silverstripe }} / PHP ${{ matrix.php }}
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
include:
# One job per supported Silverstripe major, at the oldest and newest PHP that major is
# used with here. The module's composer.json is the source of truth for the supported
# range; this matrix must follow it.
- silverstripe: '5'
php: '8.1'
recipe: '^5'
testing: '^3'
virtual: '^2'
subsites: '^3'
- silverstripe: '5'
php: '8.3'
recipe: '^5'
testing: '^3'
virtual: '^2'
subsites: '^3'
- silverstripe: '6'
php: '8.3'
recipe: '^6'
testing: '^4'
virtual: '^3'
subsites: '^4'
- silverstripe: '6'
php: '8.4'
recipe: '^6'
testing: '^4'
virtual: '^3'
subsites: '^4'

services:
database:
# MariaDB rather than MySQL: it matches both the local dev machines and the Forge
# servers, so a version-specific failure here means something real.
image: mariadb:11.4
env:
MARIADB_ROOT_PASSWORD: root
MARIADB_DATABASE: ss_ci
ports:
- 3306:3306
options: >-
--health-cmd="healthcheck.sh --connect --innodb_initialized"
--health-interval=10s --health-timeout=5s --health-retries=10

env:
SS_DATABASE_CLASS: MySQLDatabase
SS_DATABASE_SERVER: 127.0.0.1
SS_DATABASE_PORT: 3306
SS_DATABASE_USERNAME: root
SS_DATABASE_PASSWORD: root
SS_DATABASE_NAME: ss_ci
# dev: the admin/blocktypeicons route (and its test) is only configured in dev
SS_ENVIRONMENT_TYPE: dev

steps:
- name: Check out the module
uses: actions/checkout@v4
with:
path: module

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mbstring, intl, pdo_mysql, mysqli, gd, zip, dom, xml, tokenizer, simplexml, ctype, hash, session
tools: composer:v2
coverage: none

- name: Build a host project
# The module cannot be tested on its own: it needs a booted Silverstripe app. The host
# composer.json is written by hand; recipe-cms is required so the host is a realistic
# site (the recipe installs the app/ project files, including the Page class the module's
# config extends).
#
# elemental-virtual and subsites are only SUGGESTED by the module, but it ships code for
# both, so they are installed here to test it. Their tests skip themselves when the package
# is missing.
#
# The module is pulled in through a PATH repository pointing at the checkout, symlinked
# rather than mirrored ON PURPOSE: /tests is export-ignore in .gitattributes, so a
# mirrored (dist-style) install would contain no tests at all.
run: |
mkdir -p app-host
cd app-host
cat > composer.json <<JSON
{
"name": "restruct/ci-host",
"require": {
"silverstripe/recipe-cms": "${{ matrix.recipe }}",
"silverstripe/recipe-testing": "${{ matrix.testing }}",
"dnadesign/silverstripe-elemental-virtual": "${{ matrix.virtual }}",
"silverstripe/subsites": "${{ matrix.subsites }}",
"restruct/silverstripe-blockbase": "*"
},
"repositories": [
{ "type": "path", "url": "../module", "options": { "symlink": true } }
],
"autoload-dev": {
"psr-4": {
"Restruct\\\\Silverstripe\\\\BlockBase\\\\Tests\\\\": "vendor/restruct/silverstripe-blockbase/tests/"
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"config": { "allow-plugins": true }
}
JSON
# Every Silverstripe 5 release is covered by advisory PKSA-pjvm-vnw2-n3m2 (range
# <6.2.1), which will not be patched on the 5 line, so Composer refuses to install it
# under the default policy. Disabled HERE ONLY, in a throwaway CI host project.
composer config policy.advisories.block false
composer update --no-interaction --no-progress

- name: Configure the test suite
run: |
cd app-host
cat > phpunit.xml <<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<!-- Warning-to-error escalation is the RUNNER's behaviour and differs by PHPUnit
major: 9 (SS5) converts warnings by default, 11 (SS6) has no such attribute.
failOnWarning is pinned so a warning behaves identically on every leg.
failOnEmptyTestSuite backs up the fail-on-empty-test-suite CLI flag each leg passes. -->
<phpunit bootstrap="vendor/silverstripe/framework/tests/bootstrap.php" colors="true"
failOnWarning="true" failOnEmptyTestSuite="true">
<testsuites>
<testsuite name="blockbase">
<directory>vendor/restruct/silverstripe-blockbase/tests</directory>
</testsuite>
<testsuite name="consumer">
<directory>app/tests</directory>
</testsuite>
</testsuites>
</phpunit>
XML

- name: Add a consumer-shape database test
# A module's own suite structurally cannot catch an unbuildable fixture in its tests/ dir
# (PHPUnit loads those files, the class manifest never registers them, no tables are
# built). A DB test in the HOST project, run with a flush, builds the temp database with
# this module's tests/ in the manifest - the way a consuming project would.
run: |
mkdir -p app-host/app/tests
cat > app-host/app/tests/ConsumerDbTest.php <<'PHP'
<?php

use SilverStripe\Dev\SapphireTest;
use SilverStripe\Security\Member;

class ConsumerDbTest extends SapphireTest
{
protected $usesDatabase = true;

public function testTempDatabaseBuilds()
{
$m = Member::create(['FirstName' => 'x']);
$m->write();
$this->assertGreaterThan(0, $m->ID);
}
}
PHP

- name: Run tests
working-directory: app-host
# The tests run BEFORE the database is built (next step), against an empty ss_ci
# database, so a test that silently relies on a built database fails here.
#
# Every run flushes the manifest, and how differs per major:
# - SS5 (framework 5, PHPUnit 9): SS_PHPUNIT_FLUSH does not exist in framework 5, so it
# would be inert. The flush is a trailing `flush=1` argument, which only works AFTER an
# explicit test path - `--testsuite x flush=1` makes PHPUnit 9 open "flush=1" as a file
# and fail. So the SS5 legs run by path; phpunit.xml still supplies bootstrap and
# failOnWarning.
# - SS6 (framework 6, PHPUnit 11): a `flush=1` argument no longer works; framework 6 reads
# SS_PHPUNIT_FLUSH instead, which also combines with --testsuite.
#
# A green run is not enough on its own: a suite that silently loads no tests exits 0 on
# both PHPUnit 9 and 11. Every leg therefore passes --fail-on-empty-test-suite; PHPUnit 9.6
# accepts it too, although its --help does not list it. No exact test-count check on the
# summary line: PHPUnit 11 prints "OK, but there were issues!" (exit 0) instead of
# "OK (n tests" as soon as one deprecation is recorded, so a count grep would turn a
# passing suite red.
run: |
if [ "${{ matrix.silverstripe }}" = "5" ]; then
vendor/bin/phpunit --fail-on-empty-test-suite vendor/restruct/silverstripe-blockbase/tests flush=1
vendor/bin/phpunit --fail-on-empty-test-suite app/tests flush=1
else
SS_PHPUNIT_FLUSH=1 vendor/bin/phpunit --testsuite blockbase --fail-on-empty-test-suite
SS_PHPUNIT_FLUSH=1 vendor/bin/phpunit --testsuite consumer --fail-on-empty-test-suite
fi

- name: Boot the app and build the database
# The suite proves the classes behave; this proves the module does not break a real boot.
# 1.x fatals here on Silverstripe 5 and 6, on every flush (BlockIconsPreviewController::index()).
# The sake CLI differs per major. config:audit (SS6 only) lists YAML keyed to properties
# that no class declares; it is informational here (it also lists other modules' keys), and
# it does NOT catch elemental's old 'description' key (measured: the key was put back on
# ElementVirtual and the audit output did not change); ModuleConfigTest and
# ElementVirtualExtensionTest cover it.
working-directory: app-host
run: |
if [ "${{ matrix.silverstripe }}" = "5" ]; then
vendor/bin/sake dev/build flush=1
else
vendor/bin/sake db:build --flush
vendor/bin/sake config:audit
fi

lint:
name: Lint PHP ${{ matrix.php }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# The floor and the ceiling of the PHP versions the supported Silverstripe majors run on.
php: ['8.1', '8.4']
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
- name: Syntax check every PHP file
run: find src tests -name '*.php' -print0 | xargs -0 -n1 php -l
67 changes: 67 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Changelog

## 2.0.0 (2026-09-25)

**Silverstripe 5 and 6.** One line, `main`, supports both. Silverstripe 4 stays on the `1.0.x` tags;
nothing here is backported. Upgrade guide: [UPGRADING.md](UPGRADING.md).

Requires PHP `^8.1`, `silverstripe/framework ^5 || ^6`, `dnadesign/silverstripe-elemental ^5.4 || ^6`
and `restruct/silverstripe-admintweaks ^3 || ^4`.

### Changed

- **Block descriptions use `class_description`.** Elemental 5.4 no longer reads the `description`
config and elemental 6 removed it, together with `BaseElement::getDescription()`. The module's own
config (`BlockContent`, `BaseElement`, `ElementVirtual`) now sets `class_description`; project blocks
must do the same (see UPGRADING). `BlockBase::getType()` returns the class description, or the
singular name when a class has none.
- New public method `getBlockDescription()` on every block (from `ElementBlockBaseExtension`): the class
description, falling back to the type name. It replaces the `getDescription()` calls in the default
block title and the editor summary.
- The extensions now extend `SilverStripe\Core\Extension` instead of `DataExtension` (removed in
Silverstripe 6).
- The block-type icon preview (`admin/blocktypeicons`) renders from
`templates/Restruct/Silverstripe/BlockBase/Dev/BlockIconsPreview.ss` instead of an inline template
string, and lists every `BaseElement` subclass. It is no longer a CMS menu item, and outside dev it
answers 404 (before, the admin routed it at `admin/admin/blocktypeicons` in every environment and
the menu listed it under its class name).
- `composer.json`: PHP floor, `suggest` entries for `dnadesign/silverstripe-elemental-virtual` and
`silverstripe/subsites`, a `funding` entry, a `2.x-dev` branch alias.
- Licence copyright line reads "Restruct".

### Fixed

- **Every flush fataled on Silverstripe 5 and 6**: `BlockIconsPreviewController::index()` was not
compatible with `LeftAndMain::index(HTTPRequest $request): HTTPResponse`, so `dev/build` and
`?flush=1` died before building anything.
- **The icon preview listed nothing**: it asked for subclasses of `Block::class`, which resolved to the
non-existent `Restruct\Silverstripe\BlockBase\Dev\Block`.
- **`BlockContent::FieldEnabled()` answered crossed**: `'Content'` returned `has_image` and `'Image'`
returned `has_content`.
- **`ElementContentMigrationExtension` had no effect**: `updateIsMigratable()` and
`updatePageShouldSkip()` took their first argument by value, so excluding page types and skipping
pages that already had blocks never reached `MigrateContentToElement`.
- **Opening a block in the Content Blocks admin threw a TypeError on Silverstripe 6**:
`addFieldsToTab()` was given a `FieldList` instead of an array.
- `ElementVirtualExtension::LinkedElementRelation()` threw an `InvalidArgumentException` on a clone
without a linked block yet (it passed `NULL` to `UnsavedRelationList::add()`). Only reachable through
a custom field that uses `LinkedElementRelation`; the module's own linked-block field writes
`LinkedElementID` directly.
- `getExtraData()` passed `NULL` to `json_decode()` for a block without ExtraData (deprecated since
PHP 8.1).
- On Silverstripe 6: the page edit link in the block admin uses `getCMSEditLink()`, and the
`ArrayList` class is picked per major.

### Added

- Test suite (54 tests, identical on Silverstripe 5 and 6) with a regression test for each fix above,
and a GitHub Actions matrix: Silverstripe 5 on PHP 8.1 and 8.3, Silverstripe 6 on PHP 8.3 and 8.4,
with elemental-virtual and subsites installed.
- README: what installing the module configures, every config option, the public API, the optional
integrations, running the tests, and a version compatibility table.

### Notes

- No open GitHub issues or pull requests existed for this repository at release time.
- Consumers counted locally only: no local project requires this package; `restruct/silverstripe-newsgrid`
suggests it (its `BlockNewsItems` block extends `BlockContent`). No hosting-wide inventory was made.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2022 Restruct web & apps
Copyright (c) 2022 Restruct

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
Loading
Loading