brannow / typo3-dev-springboard
Launch into TYPO3 development instantly - ephemeral TYPO3 instances for extension development
dev-main
2025-09-07 10:51 UTC
Requires
- php: ^8.1
- ext-pdo: *
- symfony/filesystem: *
- symfony/yaml: *
- typo3/minimal: ^12.4 || ^13.4
This package is auto-updated.
Last update: 2025-09-07 10:51:40 UTC
README
Ephemeral TYPO3 instances for extension development. Resets to clean state on every execution.
Installation
composer require --dev brannow/typo3-dev-springboard
Requirements
- PHP >=8.1
- ext-pdo
- typo3/minimal ^12.4 || ^13.4
- symfony/filesystem
- symfony/yaml
Architecture Notes
- Features declare dependencies
- Builder resolves dependencies automatically
- Circular dependencies are detected
- Each execution starts fresh (unless
persistCache()
) - SQLite database by default
- Topological sorting ensures correct execution order
Minimal Start
use Typo3DevSpringboard\{Builder, Typo3Version}; Builder::make(Typo3Version::TYPO3_13_LTS) ->installDir(__DIR__ . '/.Build') ->build() ->execute();
Basic Frontend Page
Builder::make(Typo3Version::TYPO3_13_LTS) ->installDir(__DIR__ . '/.Build') ->withRequest('/') ->addDatabasePageRecord([ 'uid' => 1, 'title' => 'Homepage', 'slug' => '/' ]) ->addDatabaseContentRecord([ 'pid' => 1, 'header' => 'Hello', 'bodytext' => '<p>World</p>', 'CType' => 'text' ]) ->addDatabaseTypoScriptTemplate(' page = PAGE page.10 = TEXT page.10.value = Hello World ', 1) ->build() ->execute();
Builder Methods
Configuration
/* composer.json the vendor and public dir must be in based in that root directory (in our case ./.Build) "config": { "vendor-dir": ".Build/vendor", // important - same baseDir "bin-dir": ".Build/bin", "sort-packages": true, "allow-plugins": { "typo3/class-alias-loader": true, "typo3/cms-composer-installers": true } }, "extra": { "typo3/cms": { "cms-package-dir": "{$vendor-dir}/typo3/cms", "web-dir": ".Build/public" // important - same baseDir } } */ ->installDir(__DIR__ . '/.Build') // Required: TYPO3 installation path look in the composer.json example above ->persistCache() // Keep cache between executions
Request
->withRequest('/') ->withRequest('/page', 'example.local', 'POST', true) // uri, domain, method, https
Database
->addDatabasePageRecord(['title' => 'Page', 'slug' => '/']) ->addDatabaseContentRecord(['pid' => 1, 'CType' => 'text']) ->addDatabaseTypoScriptTemplate($typoscript, $pageId, $root, $name)
Site Configuration
->setSiteRootPageId(1) ->addSiteLanguage(SiteLanguage::EN, SiteLanguage::DE) ->setSiteLanguage(SiteLanguage::DE, 1) // Force language ID ->setSiteConfig([...]) // Complete override
Execution
->build() // Prepare system ->execute() // Run TYPO3 ->execute(true) // Return output instead of printing
Custom Tables
Quick Method - GenericTable
$database = $builder->getFeature(\Typo3DevSpringboard\Feature\Database::class); $genericTable = $database->getTable(\Typo3DevSpringboard\Feature\Database\GenericTable::class) ->addTableSchema('tx_extension_table', [ 'uid' => 'INTEGER PRIMARY KEY AUTOINCREMENT', 'pid' => 'INTEGER DEFAULT 0', 'title' => 'TEXT' ]); $database->addRowToTable($genericTable, 'tx_extension_table', ['title' => 'Test', 'pid' => 1]);
Custom Table Class
use Typo3DevSpringboard\Feature\Database\Table; class ExtensionTable extends Table { public static function getIdentifier(): string { return 'extension_table'; } protected function getTableSchemas(): array { return [ 'tx_extension_table' => [ 'uid' => 'INTEGER PRIMARY KEY AUTOINCREMENT', 'pid' => 'INTEGER DEFAULT 0', 'title' => 'TEXT' ] ]; } } $database->getTable(ExtensionTable::class) ->addRow(['title' => 'Test', 'pid' => 1], 'tx_extension_table');
Custom Features
Implement Feature
use Typo3DevSpringboard\Feature\Typo3FeatureInterface; class CustomFeature implements Typo3FeatureInterface { public static function make(Typo3Version $version): static { return new static($version); } public static function getIdentifier(): string { return 'custom'; } public function requiredFeatureIdentifier(): array { return []; // Dependencies } public function execute(array $features): static { // Your logic here return $this; } } $builder->addFeature(CustomFeature::make(Typo3Version::TYPO3_13_LTS));
Override Existing Feature
class CustomDatabase extends \Typo3DevSpringboard\Feature\Database { // Override methods } $builder->addFeature(CustomDatabase::make(Typo3Version::TYPO3_13_LTS));
Anonymous Class - New Feature
$customFeature = new class(Typo3Version::TYPO3_13_LTS) implements Typo3FeatureInterface { public function __construct( private readonly Typo3Version $version ) {} public static function make(Typo3Version $version): static { return new static($version); } public static function getIdentifier(): string { return 'anonymous_feature'; } public function requiredFeatureIdentifier(): array { return ['Database']; } public function execute(array $features): static { $database = $features['Database']; // Custom logic return $this; } }; $builder->addFeature($customFeature);
Anonymous Class - Override Feature
// Override existing Database feature with anonymous class $customDatabase = new class(Typo3Version::TYPO3_13_LTS) extends \Typo3DevSpringboard\Feature\Database { public static function getIdentifier(): string { return 'Database'; // Same identifier to override } public function execute(array $features): static { // Custom database setup parent::execute($features); // Additional logic return $this; } }; $builder->addFeature($customDatabase);
Anonymous Class - Custom Table
$database = $builder->getFeature(Database::class); $customTable = new class(Typo3Version::TYPO3_13_LTS) extends Table { public function __construct( private readonly Typo3Version $version ) {} public static function make(Typo3Version $version): static { return new static($version); } public static function getIdentifier(): string { return 'anonymous_table'; } protected function getTableSchemas(): array { return [ 'tx_anonymous_table' => [ 'uid' => 'INTEGER PRIMARY KEY AUTOINCREMENT', 'data' => 'TEXT' ] ]; } }; $database->addTable($customTable); $customTable->addRow(['data' => 'test'], 'tx_anonymous_table');
Feature Access
// Get feature $database = $builder->getFeature(\Typo3DevSpringboard\Feature\Database::class); $fileSystem = $builder->getFeature(\Typo3DevSpringboard\Feature\FileSystem::class); $request = $builder->getFeature(\Typo3DevSpringboard\Feature\Request::class); $site = $builder->getFeature(\Typo3DevSpringboard\Feature\Site::class); // Remove feature $builder->removeFeature(\Typo3DevSpringboard\Feature\Site::class);
Core Features
- Request: HTTP request simulation
- FileSystem: Directory and config file management
- Site: Site configuration (config.yaml)
- Database: SQLite database and tables
Built-in Tables
- Pages: pages table
- TtContent: tt_content table
- Template: sys_template table
- Caches: All cache tables
- GenericTable: Custom table handler
Testing Example
class ExtensionTest extends \PHPUnit\Framework\TestCase { public function testOutput(): void { $output = Builder::make(Typo3Version::TYPO3_13_LTS) ->installDir(__DIR__ . '/../.Build') ->withRequest('/test') ->build() ->execute(true); $this->assertStringContainsString('expected', $output); } }
CLI Usage
$uri = $_SERVER['argv'][1] ?? '/'; Builder::make(Typo3Version::TYPO3_13_LTS) ->installDir(__DIR__ . '/.Build') ->withRequest($uri) ->build() ->execute();
Complex Example
// Custom feature with dependencies class MyFeature implements Typo3FeatureInterface { public function requiredFeatureIdentifier(): array { return ['Database', 'FileSystem']; } public function execute(array $features): static { $database = $features['Database']; $fileSystem = $features['FileSystem']; // Use dependencies return $this; } } // Custom table with default values $genericTable = $database->getTable(GenericTable::class) ->addTableSchema('tx_ext_table', [...]) ->withDefaultDataCallable(function($data, $table) { $data['tstamp'] ??= time(); $data['crdate'] ??= time(); return $data; }); // Multiple languages with specific IDs $builder ->setSiteLanguage(SiteLanguage::EN, 0) ->setSiteLanguage(SiteLanguage::DE, 1) ->addSiteLanguage(/* custom languages */); // Custom site configuration $builder->setSiteConfig([ 'rootPageId' => 1, 'base' => 'https://example.com/', 'languages' => [/* custom */] ]);
Additional Information
FeatureSystem.md a Deepdive into the Feature System and how it works
License
MIT