Skip to content

Instantly share code, notes, and snippets.

@maikschneider
Last active February 12, 2025 12:47
Show Gist options
  • Select an option

  • Save maikschneider/7a3df2a210b54900e21dee361dea64cb to your computer and use it in GitHub Desktop.

Select an option

Save maikschneider/7a3df2a210b54900e21dee361dea64cb to your computer and use it in GitHub Desktop.
Check if all TYPO3 extensions are required in sitepackage's composer.json
#!/usr/bin/env php
<?php
/**
* TYPO3 Sitepackage Requirement Checker
* This script checks if all TYPO3 extensions (type: typo3-cms-extension) from the root composer.json
* are properly required in a sitepackage's composer.json file.
* Usage:
* ./sitepackage-req-checker.php packages/xima_sitepackage/composer.json
* ./sitepackage-req-checker.php packages/xima_sitepackage/composer.json --ci=reports/junit.xml
*/
class RequirementChecker
{
private const ERROR_COLOR = "\e[0;31m";
private const SUCCESS_COLOR = "\e[0;32m";
private const INFO_COLOR = "\e[0;36m";
private const RESET_COLOR = "\e[0m";
private string $rootDir;
private string $rootComposerFile;
private string $vendorDir;
private ?string $sitePackageComposerJson;
private ?string $ciReportPath;
private array $errors = [];
private array $warnings = [];
public function __construct()
{
$this->rootDir = getcwd();
$this->rootComposerFile = $this->rootDir . "/composer.json";
$this->vendorDir = $this->rootDir . "/vendor";
$this->sitePackageComposerJson = null;
$this->ciReportPath = null;
}
/**
* Validate environment and input parameters
*/
public function validateEnvironment(array $argv): bool
{
if (!$this->parseArguments($argv)) {
return false;
}
// Check if composer.json exists
if (!file_exists($this->rootComposerFile)) {
$this->addError("Root composer.json is not present in the current directory.");
return false;
}
// Check if vendor directory exists
if (!is_dir($this->vendorDir)) {
$this->addError("Vendor directory not present in the current directory.");
return false;
}
// Validate sitepackage composer.json path
if (!file_exists($this->sitePackageComposerJson)) {
$this->addError("'{$this->sitePackageComposerJson}' not found.");
return false;
}
return true;
}
/**
* Parse command line arguments
*/
private function parseArguments(array $argv): bool
{
// Check for sitepackage path
if (!isset($argv[1])) {
$this->addError("Provide relative path to composer.json of sitepackage as the first argument.");
$this->printMessage("Usage: './sitepackage-req-checker.php packages/xima_sitepackage/composer.json [--ci=reports/junit.xml]'",
self::INFO_COLOR);
return false;
}
$this->sitePackageComposerJson = $this->rootDir . "/" . $argv[1];
// Check for CI report argument
if (isset($argv[2])) {
if (!str_starts_with($argv[2], '--ci')) {
$this->addError("Invalid argument format. Use --ci=path/to/report.xml");
return false;
}
$this->ciReportPath = substr($argv[2], 5); // Remove '--ci=' prefix
if (empty($this->ciReportPath)) {
$this->ciReportPath = 'junit.xml'; // Default filename if no path provided
}
// Ensure directory exists
$reportDir = dirname($this->ciReportPath);
if (!is_dir($reportDir) && !mkdir($reportDir, 0777, true) && !is_dir($reportDir)) {
$this->addError("Could not create directory for CI report: {$reportDir}");
return false;
}
}
return true;
}
/**
* Add error message to collection
*/
private function addError(string $message): void
{
$this->errors[] = $message;
$this->printMessage("Error: " . $message, self::ERROR_COLOR);
}
/**
* Print colored message to console
*/
private function printMessage(string $message, string $color = self::INFO_COLOR): void
{
printf($color . $message . self::RESET_COLOR . PHP_EOL);
}
/**
* Check requirements between root composer.json and sitepackage
*/
public function checkRequirements(): bool
{
try {
// Load composer files
$rootComposerData = $this->loadJsonFile($this->rootComposerFile);
$sitePackageComposerData = $this->loadJsonFile($this->sitePackageComposerJson);
// Validate sitepackage composer.json structure
if (!isset($sitePackageComposerData['require'])) {
$this->addError("'require' object is missing in sitepackage composer.json.");
return false;
}
if (!isset($sitePackageComposerData['name'])) {
$this->addError("'name' is missing in sitepackage composer.json.");
return false;
}
// Get all dependencies excluding the sitepackage itself
$sitePackageName = $sitePackageComposerData['name'];
$requires = array_keys(array_diff_key($rootComposerData['require'], [$sitePackageName => null]));
// Check for missing TYPO3 extensions
$packagesMissing = $this->findMissingPackages($requires, $sitePackageComposerData['require']);
if (!empty($packagesMissing)) {
$this->printMessage("The following TYPO3 extensions are missing in the sitepackage:", self::ERROR_COLOR);
foreach ($packagesMissing as $missing) {
$this->printMessage(" - {$missing}", self::ERROR_COLOR);
}
$this->printMessage("\nPlease add them to '{$this->sitePackageComposerJson}'", self::ERROR_COLOR);
// Generate CI report if requested
$this->generateJUnitReport($packagesMissing);
return false;
}
// Generate CI report even when successful
$this->generateJUnitReport([]);
$this->printMessage("Success: All required TYPO3 extensions are properly included!", self::SUCCESS_COLOR);
return true;
} catch (Exception $e) {
$this->addError($e->getMessage());
$this->generateJUnitReport([]);
return false;
}
}
/**
* Load and decode JSON file
*/
private function loadJsonFile(string $path): array
{
$content = file_get_contents($path);
if ($content === false) {
throw new Exception("Failed to read file: {$path}");
}
$data = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("Invalid JSON in file: {$path} - " . json_last_error_msg());
}
return $data;
}
/**
* Find missing TYPO3 extensions
*/
private function findMissingPackages(array $requires, array $sitePackageRequires): array
{
$packagesMissing = [];
foreach ($requires as $require) {
$composerJsonPath = $this->vendorDir . '/' . $require . '/composer.json';
if (file_exists($composerJsonPath)) {
try {
$packageData = $this->loadJsonFile($composerJsonPath);
if (isset($packageData['type']) && $packageData['type'] === 'typo3-cms-extension') {
if (!isset($sitePackageRequires[$require])) {
$packagesMissing[] = $require;
}
}
} catch (Exception $e) {
$this->addWarning("Could not process {$require}: " . $e->getMessage());
}
}
}
return array_unique($packagesMissing);
}
/**
* Add warning message to collection
*/
private function addWarning(string $message): void
{
$this->warnings[] = $message;
$this->printMessage("Warning: " . $message, self::INFO_COLOR);
}
/**
* Generate JUnit XML report
*/
private function generateJUnitReport(array $packagesMissing): void
{
if (!$this->ciReportPath) {
return;
}
$timestamp = date('c');
$testCount = count($packagesMissing) + count($this->warnings);
$failures = count($packagesMissing);
$errors = count($this->errors);
$xml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><testsuites/>');
$testsuite = $xml->addChild('testsuite');
$testsuite->addAttribute('name', 'TYPO3 Sitepackage Requirements');
$testsuite->addAttribute('tests', $testCount);
$testsuite->addAttribute('failures', $failures);
$testsuite->addAttribute('errors', $errors);
$testsuite->addAttribute('timestamp', $timestamp);
// Add missing packages as test failures
foreach ($packagesMissing as $package) {
$testcase = $testsuite->addChild('testcase');
$testcase->addAttribute('name', "Check requirement: {$package}");
$testcase->addAttribute('classname', 'RequirementChecker');
$failure = $testcase->addChild('failure');
$failure->addAttribute('type', 'MissingRequirement');
$failure->addAttribute('message', "Package {$package} is missing in sitepackage requirements");
}
// Add warnings as successful tests with system-out
foreach ($this->warnings as $warning) {
$testcase = $testsuite->addChild('testcase');
$testcase->addAttribute('name', "Warning: " . substr($warning, 0, 50) . "...");
$testcase->addAttribute('classname', 'RequirementChecker');
$systemOut = $testcase->addChild('system-out');
$systemOut[0] = $warning;
}
// Save the XML file
$dom = new DOMDocument('1.0');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->loadXML($xml->asXML() ?: '');
if (!$dom->save($this->ciReportPath)) {
$this->addError("Failed to write CI report to {$this->ciReportPath}");
} else {
$this->printMessage("JUnit report generated: {$this->ciReportPath}", self::SUCCESS_COLOR);
}
}
}
// Run the checker
$checker = new RequirementChecker();
if ($checker->validateEnvironment($argv)) {
exit($checker->checkRequirements() ? 0 : 1);
}
exit(1);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment