From 4eeb092af91114b22af453c1be85135b1738311c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Fri, 29 May 2026 15:14:44 +0200 Subject: [PATCH] Massive infra update --- .castor/context.php | 95 ++ .castor/docker.php | 375 ++++-- .castor/init.php | 78 ++ .castor/qa.php | 92 +- .env | 2 +- .env.test | 2 +- .github/workflows/ci.yml | 14 +- .gitignore | 7 +- .home/.gitignore | 0 .php-cs-fixer.php | 15 +- castor.php | 67 +- foobar.sh | 7 + .../docker/docker-compose.builder.yml | 30 - infrastructure/docker/docker-compose.dev.yml | 14 + infrastructure/docker/docker-compose.yml | 99 +- infrastructure/docker/services/php/Dockerfile | 66 +- .../docker/services/php/base/sudo.sh | 4 + .../services/php/builder/etc/sudoers.d/sudo | 1 - infrastructure/docker/services/php/entrypoint | 26 - .../php/frontend/etc/nginx/nginx.conf | 8 +- .../etc/service/nginx/supervise/.gitignore | 2 + .../etc/service/php-fpm/supervise/.gitignore | 2 + .../php-configuration/fpm/php-fpm.conf | 3 - .../docker/services/router/Dockerfile | 2 +- .../services/router/traefik/traefik.yaml | 6 +- phpstan.neon | 15 +- phpunit.xml.dist | 2 +- public/index.php | 2 +- src/Controller/HomepageController.php | 2 +- src/Controller/SlackController.php | 4 +- .../Type/DateTimeImmutableWithMillis.php | 4 +- src/EventSubscriber/ChallengeSubscriber.php | 2 +- src/Slack/DebtListBlockBuilder.php | 2 +- tests/Acceptance/AcceptenceTest.php | 64 +- .../ControlTower/DebtCreatorTest.php | 18 +- tests/bootstrap.php | 2 +- tools/bin/twig-cs-fixer | 1 + tools/php-cs-fixer/composer.json | 4 +- tools/php-cs-fixer/composer.lock | 766 ++++++----- tools/phpstan/composer.json | 7 +- tools/phpstan/composer.lock | 110 +- tools/twig-cs-fixer/.gitignore | 1 + tools/twig-cs-fixer/composer.json | 13 + tools/twig-cs-fixer/composer.lock | 1118 +++++++++++++++++ 44 files changed, 2475 insertions(+), 679 deletions(-) create mode 100644 .castor/context.php create mode 100644 .castor/init.php create mode 100644 .home/.gitignore create mode 100644 foobar.sh delete mode 100644 infrastructure/docker/docker-compose.builder.yml create mode 100644 infrastructure/docker/docker-compose.dev.yml create mode 100644 infrastructure/docker/services/php/base/sudo.sh delete mode 100644 infrastructure/docker/services/php/builder/etc/sudoers.d/sudo delete mode 100755 infrastructure/docker/services/php/entrypoint create mode 100644 infrastructure/docker/services/php/frontend/etc/service/nginx/supervise/.gitignore create mode 100644 infrastructure/docker/services/php/frontend/etc/service/php-fpm/supervise/.gitignore create mode 120000 tools/bin/twig-cs-fixer create mode 100644 tools/twig-cs-fixer/.gitignore create mode 100644 tools/twig-cs-fixer/composer.json create mode 100644 tools/twig-cs-fixer/composer.lock diff --git a/.castor/context.php b/.castor/context.php new file mode 100644 index 0000000..bfd7a46 --- /dev/null +++ b/.castor/context.php @@ -0,0 +1,95 @@ + 'app', + 'root_domain' => 'app.test', + 'extra_domains' => [], + 'php_version' => '8.5', + 'docker_compose_files' => [ + 'docker-compose.yml', + 'docker-compose.dev.yml', + ], + 'docker_compose_run_environment' => [], + 'macos' => false, + 'power_shell' => false, + // check if posix_geteuid is available, if not, use getmyuid (windows) + 'user_id' => \function_exists('posix_geteuid') ? posix_geteuid() : getmyuid(), + 'root_dir' => \dirname(__DIR__), + ]; + + if (file_exists($data['root_dir'] . '/infrastructure/docker/docker-compose.override.yml')) { + $data['docker_compose_files'][] = 'docker-compose.override.yml'; + } + + $platform = strtolower(php_uname('s')); + if (str_contains($platform, 'darwin')) { + $data['macos'] = true; + } elseif (\in_array($platform, ['win32', 'win64', 'windows nt'], true)) { + $data['power_shell'] = true; + } + + // 2³² - 1 + if (false === $data['user_id'] || $data['user_id'] > 4294967295) { + $data['user_id'] = 1000; + } + + if (0 === $data['user_id']) { + log('Running as root? Fallback to fake user id.', 'warning'); + $data['user_id'] = 1000; + } + + return new Context( + $data, + pty: Process::isPtySupported(), + environment: [ + 'BUILDKIT_PROGRESS' => 'plain', + ] + ); +} + +#[AsContext(name: 'test')] +function create_test_context(): Context +{ + $c = create_default_context(); + + return $c + ->withEnvironment([ + 'APP_ENV' => 'test', + ]) + ; +} + +#[AsContext(name: 'ci')] +function create_ci_context(): Context +{ + $c = create_test_context(); + + return $c + ->withData( + // override the default context here + [ + 'docker_compose_files' => [ + 'docker-compose.yml', + // Usually, the following service is not be needed in the CI + 'docker-compose.dev.yml', + // 'docker-compose.ci.yml', + ], + ], + recursive: false + ) + ->withEnvironment([ + 'COMPOSE_ANSI' => 'never', + ]) + ; +} diff --git a/.castor/docker.php b/.castor/docker.php index a7063dc..6fffbda 100644 --- a/.castor/docker.php +++ b/.castor/docker.php @@ -2,24 +2,24 @@ namespace docker; -use Castor\Attribute\AsContext; use Castor\Attribute\AsOption; +use Castor\Attribute\AsRawTokens; use Castor\Attribute\AsTask; use Castor\Context; +use Castor\Helper\PathHelper; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Process\Exception\ExceptionInterface; +use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; use Symfony\Contracts\HttpClient\Exception\ExceptionInterface as HttpExceptionInterface; -use function Castor\cache; use function Castor\capture; use function Castor\context; use function Castor\finder; use function Castor\fs; use function Castor\http_client; use function Castor\io; -use function Castor\log; use function Castor\open; use function Castor\run; use function Castor\variable; @@ -58,7 +58,7 @@ function about(): void } catch (HttpExceptionInterface) { } - io()->listing(array_map(fn ($url) => "https://{$url}", array_unique($urls))); + io()->listing(array_map(static fn ($url) => "https://{$url}", array_unique($urls))); } #[AsTask(description: 'Opens the project in your browser', namespace: '', aliases: ['open'])] @@ -69,25 +69,26 @@ function open_project(): void #[AsTask(description: 'Builds the infrastructure', aliases: ['build'])] function build( + #[AsOption(description: 'The service to build (default: all services)', autocomplete: 'docker\get_service_names')] ?string $service = null, ?string $profile = null, ): void { + generate_certificates(force: false); + io()->title('Building infrastructure'); $command = []; + $command[] = '--profile'; if ($profile) { - $command[] = '--profile'; $command[] = $profile; } else { - $command[] = '--profile'; - $command[] = 'default'; + $command[] = '*'; } $command = [ ...$command, 'build', - '--build-arg', 'USER_ID=' . variable('user_id'), '--build-arg', 'PHP_VERSION=' . variable('php_version'), '--build-arg', 'PROJECT_NAME=' . variable('project_name'), ]; @@ -96,13 +97,7 @@ function build( $command[] = $service; } - docker_compose($command, withBuilder: true); -} - -#[AsTask(description: 'Pull images from the registry')] -function pull(): void -{ - docker_compose(['pull', '-q'], withBuilder: true); + docker_compose($command); } /** @@ -110,6 +105,7 @@ function pull(): void */ #[AsTask(description: 'Builds and starts the infrastructure', aliases: ['up'])] function up( + #[AsOption(description: 'The service to start (default: all services)', autocomplete: 'docker\get_service_names')] ?string $service = null, #[AsOption(mode: InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED)] array $profiles = [], @@ -127,7 +123,7 @@ function up( try { docker_compose($command, profiles: $profiles); } catch (ExceptionInterface $e) { - io()->error('An error occured while starting the infrastructure.'); + io()->error('An error occurred while starting the infrastructure.'); io()->note('Did you forget to run "castor docker:build"?'); io()->note('Or you forget to login to the registry?'); @@ -140,6 +136,7 @@ function up( */ #[AsTask(description: 'Stops the infrastructure', aliases: ['stop'])] function stop( + #[AsOption(description: 'The service to stop (default: all services)', autocomplete: 'docker\get_service_names')] ?string $service = null, #[AsOption(mode: InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED)] array $profiles = [], @@ -157,16 +154,22 @@ function stop( docker_compose($command, profiles: $profiles); } -#[AsTask(description: 'Opens a shell (bash) into a builder container', aliases: ['builder'])] -function builder(): void +/** + * @param array $params + */ +#[AsTask(description: 'Opens a shell (bash) or proxy any command to the builder container', aliases: ['builder'])] +function builder(#[AsRawTokens] array $params = []): int { - $c = context() - ->withTimeout(null) - ->withTty() - ->withEnvironment($_ENV + $_SERVER) - ->withAllowFailure() - ; - docker_compose_run('bash', c: $c); + $c = context()->withEnvironment($_ENV + $_SERVER); + + if (0 === \count($params)) { + $params = ['bash']; + $c = $c->toInteractive(); + } else { + $c = $c->withTty(false)->withPty(false)->withAllowFailure(); + } + + return (int) docker_compose_run(implode(' ', $params), c: $c)->getExitCode(); } /** @@ -188,9 +191,23 @@ function logs( } #[AsTask(description: 'Lists containers status', aliases: ['ps'])] -function ps(): void +function ps(bool $ports = false): void { - docker_compose(['ps'], withBuilder: false); + $command = [ + 'ps', + '--format', 'table {{.Name}}\t{{.Image}}\t{{.Status}}\t{{.RunningFor}}\t{{.Command}}', + '--no-trunc', + ]; + + if ($ports) { + $command[2] .= '\t{{.Ports}}'; + } + + docker_compose($command, profiles: ['*']); + + if (!$ports) { + io()->comment('You can use the "--ports" option to display ports.'); + } } #[AsTask(description: 'Cleans the infrastructure (remove container, volume, networks)', aliases: ['destroy'])] @@ -210,7 +227,7 @@ function destroy( } } - docker_compose(['down', '--remove-orphans', '--volumes', '--rmi=local'], withBuilder: true); + docker_compose(['down', '--remove-orphans', '--volumes', '--rmi=local'], profiles: ['*']); $files = finder() ->in(variable('root_dir') . '/infrastructure/docker/services/router/certs/') ->name('*.pem') @@ -293,141 +310,93 @@ function workers_start(): void { io()->title('Starting workers'); - up(profiles: ['worker']); -} - -#[AsTask(description: 'Stops the workers', namespace: 'docker:worker', name: 'stop', aliases: ['stop-workers'])] -function workers_stop(): void -{ - io()->title('Stopping workers'); - - stop(profiles: ['worker']); -} - -#[AsContext(default: true)] -function create_default_context(): Context -{ - $data = create_default_variables() + [ - 'project_name' => 'app', - 'root_domain' => 'app.test', - 'extra_domains' => [], - 'project_directory' => 'application', - 'php_version' => '8.2', - 'docker_compose_files' => [ - 'docker-compose.yml', - ], - 'macos' => false, - 'power_shell' => false, - // check if posix_geteuid is available, if not, use getmyuid (windows) - 'user_id' => \function_exists('posix_geteuid') ? posix_geteuid() : getmyuid(), - 'root_dir' => \dirname(__DIR__), - ]; - - if (file_exists($data['root_dir'] . '/infrastructure/docker/docker-compose.override.yml')) { - $data['docker_compose_files'][] = 'docker-compose.override.yml'; - } - - // We need an empty context to run command, since the default context has - // not been set in castor, since we ARE creating it right now - $emptyContext = new Context(); + $command = ['up', '--detach', '--wait', '--no-build']; + $profiles = ['worker', 'default']; - $data['composer_cache_dir'] = cache('composer_cache_dir', function () use ($emptyContext): string { - $composerCacheDir = capture(['composer', 'global', 'config', 'cache-dir', '-q'], onFailure: '', context: $emptyContext); - // If PHP is broken, the output will not be a valid path but an error message - if (!is_dir($composerCacheDir)) { - $composerCacheDir = sys_get_temp_dir() . '/castor/composer'; - // If the directory does not exist, we create it. Otherwise, docker - // will do, as root, and the user will not be able to write in it. - if (!is_dir($composerCacheDir)) { - mkdir($composerCacheDir, 0o777, true); - } + try { + docker_compose($command, profiles: $profiles); + } catch (ProcessFailedException $e) { + preg_match('/service "(\w+)" depends on undefined service "(\w+)"/', $e->getProcess()->getErrorOutput(), $matches); + if (!$matches) { + throw $e; } - return $composerCacheDir; - }); + $r = new \ReflectionFunction(__FUNCTION__); - $platform = strtolower(php_uname('s')); - if (str_contains($platform, 'darwin')) { - $data['macos'] = true; - } elseif (\in_array($platform, ['win32', 'win64', 'windows nt'])) { - $data['power_shell'] = true; - } + io()->newLine(); + io()->error('An error occurred while starting the workers.'); + io()->warning(\sprintf( + <<<'EOT' + The "%1$s" service depends on the "%2$s" service, which is not defined in the current docker-compose configuration. - if (false === $data['user_id'] || $data['user_id'] > 256000) { - $data['user_id'] = 1000; - } + Usually, this means that the service "%2$s" is not defined in the same profile (%3$s) as the "%1$s" service. - if (0 === $data['user_id']) { - log('Running as root? Fallback to fake user id.', 'warning'); - $data['user_id'] = 1000; + You can try to add its profile in the current task: %4$s:%5$s + EOT, + $matches[1], + $matches[2], + implode(', ', $profiles), + PathHelper::makeRelative((string) $r->getFileName()), + $r->getStartLine(), + )); } - - return new Context( - $data, - pty: Process::isPtySupported(), - environment: [ - 'BUILDKIT_PROGRESS' => 'plain', - ] - ); } -#[AsContext(name: 'ci')] -function create_ci_context(): Context +#[AsTask(description: 'Stops the workers', namespace: 'docker:worker', name: 'stop', aliases: ['stop-workers'])] +function workers_stop(): void { - $c = create_default_context(); - - return $c - ->withData([ - // override the default context here - ]) - ->withEnvironment([ - 'COMPOSE_ANSI' => 'never', - ]) - ; + io()->title('Stopping workers'); + $workers = get_service_names(profile: 'worker'); + + if ([] === $workers) { + io()->error('No worker service found.'); + + return; + } + + docker_compose(['stop', ...$workers], profiles: ['*']); } /** * @param list $subCommand * @param list $profiles */ -function docker_compose(array $subCommand, ?Context $c = null, bool $withBuilder = false, array $profiles = []): Process +function docker_compose(array $subCommand, ?Context $c = null, array $profiles = []): Process { $c ??= context(); $profiles = $profiles ?: ['default']; - $domains = [variable('root_domain'), ...variable('extra_domains')]; + $domains = [$c['root_domain'], ...$c['extra_domains']]; $domains = '`' . implode('`) || Host(`', $domains) . '`'; - $c = $c - ->withTimeout(null) - ->withEnvironment([ - 'PROJECT_NAME' => variable('project_name'), - 'PROJECT_ROOT_DOMAIN' => variable('root_domain'), - 'PROJECT_DOMAINS' => $domains, - 'USER_ID' => variable('user_id'), - 'COMPOSER_CACHE_DIR' => variable('composer_cache_dir'), - 'PHP_VERSION' => variable('php_version'), - ]) - ; + $c = $c->withEnvironment([ + 'PROJECT_NAME' => $c['project_name'], + 'PROJECT_ROOT_DOMAIN' => $c['root_domain'], + 'PROJECT_DOMAINS' => $domains, + 'USER_ID' => $c['user_id'], + 'PHP_VERSION' => $c['php_version'], + 'REGISTRY' => $c['registry'] ?? '', + ]); + + if ($c['APP_ENV'] ?? false) { + $c = $c->withEnvironment([ + 'APP_ENV' => $c['APP_ENV'] ?? '', + ]); + } $command = [ 'docker', 'compose', - '-p', variable('project_name'), + '-p', $c['project_name'], ]; foreach ($profiles as $profile) { $command[] = '--profile'; $command[] = $profile; } - foreach (variable('docker_compose_files') as $file) { + foreach ($c['docker_compose_files'] as $file) { $command[] = '-f'; - $command[] = variable('root_dir') . '/infrastructure/docker/' . $file; - } - - if ($withBuilder) { - $command[] = '-f'; - $command[] = variable('root_dir') . '/infrastructure/docker/docker-compose.builder.yml'; + $command[] = $c['root_dir'] . '/infrastructure/docker/' . $file; } $command = array_merge($command, $subCommand); @@ -442,8 +411,9 @@ function docker_compose_run( bool $noDeps = true, ?string $workDir = null, bool $portMapping = false, - bool $withBuilder = true, ): Process { + $c ??= context(); + $command = [ 'run', '--rm', @@ -462,12 +432,17 @@ function docker_compose_run( $command[] = $workDir; } + foreach ($c['docker_compose_run_environment'] as $key => $value) { + $command[] = '-e'; + $command[] = "{$key}={$value}"; + } + $command[] = $service; $command[] = '/bin/bash'; $command[] = '-c'; $command[] = "{$runCommand}"; - return docker_compose($command, c: $c, withBuilder: $withBuilder); + return docker_compose($command, c: $c, profiles: ['*']); } function docker_exit_code( @@ -476,7 +451,6 @@ function docker_exit_code( string $service = 'builder', bool $noDeps = true, ?string $workDir = null, - bool $withBuilder = true, ): int { $c = ($c ?? context())->withAllowFailure(); @@ -486,7 +460,6 @@ function docker_exit_code( service: $service, noDeps: $noDeps, workDir: $workDir, - withBuilder: $withBuilder, ); return $process->getExitCode() ?? 0; @@ -498,9 +471,139 @@ function run_in_docker_or_locally_for_mac(string $command, ?Context $c = null): { $c ??= context(); - if (variable('macos')) { - run($command, context: $c->withWorkingDirectory(variable('root_dir'))); + if ($c['macos']) { + run($command, context: $c->withWorkingDirectory($c['root_dir'])); } else { docker_compose_run($command, c: $c); } } + +#[AsTask(description: 'Push images cache to the registry', namespace: 'docker', name: 'push', aliases: ['push'])] +function push(bool $dryRun = false): void +{ + $registry = variable('registry'); + + if (!$registry) { + throw new \RuntimeException('You must define a registry to push images.'); + } + + // Generate bake file + $targets = []; + + foreach (get_services() as $service => $config) { + $cacheFrom = $config['build']['cache_from'][0] ?? null; + + if (null === $cacheFrom) { + continue; + } + + $cacheFrom = explode(',', $cacheFrom); + $reference = null; + $type = null; + + if (1 === \count($cacheFrom)) { + $reference = $cacheFrom[0]; + $type = 'registry'; + } else { + foreach ($cacheFrom as $part) { + $from = explode('=', $part); + + if (2 !== \count($from)) { + continue; + } + + if ('type' === $from[0]) { + $type = $from[1]; + } + + if ('ref' === $from[0]) { + $reference = $from[1]; + } + } + } + + $targets[] = [ + 'reference' => $reference, + 'type' => $type, + 'context' => $config['build']['context'], + 'dockerfile' => $config['build']['dockerfile'] ?? 'Dockerfile', + 'target' => $config['build']['target'] ?? null, + ]; + } + + $content = \sprintf( + <<<'EOHCL' + group "default" { + targets = [%s] + } + + EOHCL, + implode(', ', array_map(static fn ($target) => \sprintf('"%s"', $target['target']), $targets)) + ); + + foreach ($targets as $target) { + $content .= \sprintf( + <<<'EOHCL' + target "%s" { + context = "%s" + dockerfile = "%s" + cache-from = ["%s"] + cache-to = ["type=%s,ref=%s,mode=max"] + target = "%s" + args = { + PHP_VERSION = "%s" + } + } + + EOHCL, + $target['target'], $target['context'], $target['dockerfile'], $target['reference'], $target['type'], $target['reference'], $target['target'], variable('php_version') + ); + } + + if ($dryRun) { + io()->write($content); + + return; + } + + // write bake file in tmp file + $bakeFile = tempnam(sys_get_temp_dir(), 'bake'); + file_put_contents($bakeFile, $content); + + // Run bake + run(['docker', 'buildx', 'bake', '-f', $bakeFile]); +} + +/** + * @return array, build: array{context: string, dockerfile?: string, cache_from?: list, target?: string}}> + */ +function get_services(?string $profile = null): array +{ + $services = json_decode( + docker_compose( + ['config', '--format', 'json'], + context()->withQuiet(), + profiles: ['*'], + )->getOutput(), + true, + flags: JSON_THROW_ON_ERROR, + )['services']; + + if (null === $profile) { + return $services; + } + + // Docker compose cannot get the services config for a given profile if one of + // these services depends on another service in another profile. + // So we find all services, in all profiles, and manually filter the one + // that has the given profile, then we stop it + return array_filter($services, static fn ($service) => \in_array($profile, $service['profiles'] ?? [], true)); +} + +/** + * @return list + */ +function get_service_names(?string $profile = null): array +{ + return array_keys(get_services($profile)); +} diff --git a/.castor/init.php b/.castor/init.php new file mode 100644 index 0000000..86c3227 --- /dev/null +++ b/.castor/init.php @@ -0,0 +1,78 @@ +remove([ + '.github/', + 'README.md', + 'CHANGELOG.md', + 'CONTRIBUTING.md', + 'LICENSE', + __FILE__, + ]); + fs()->rename('README.dist.md', 'README.md'); + + $readMeContent = file_get_contents('README.md'); + + if (false === $readMeContent) { + return; + } + + $urls = [variable('root_domain'), ...variable('extra_domains')]; + $readMeContent = str_replace('', implode(' ', $urls), $readMeContent); + file_put_contents('README.md', $readMeContent); +} + +#[AsTask(description: 'Install Symfony')] +function symfony(bool $webApp = false): void +{ + $base = rtrim(variable('root_dir') . '/application'); + + $gitIgnore = $base . '/.gitignore'; + $gitIgnoreContent = ''; + if (file_exists($gitIgnore)) { + $gitIgnoreContent = file_get_contents($gitIgnore); + } + + build(); + docker_compose_run('composer create-project symfony/skeleton sf'); + + fs()->mirror($base . '/sf/', $base, options: ['override' => true]); + fs()->remove([$base . '/sf', $base . '/var']); + + if ($webApp) { + docker_compose_run('composer require webapp'); + } + + docker_compose_run("sed -i 's#^DATABASE_URL.*#DATABASE_URL=postgresql://app:app@postgres:5432/app\\?serverVersion=16\\&charset=utf8#' .env"); + file_put_contents($gitIgnore, $gitIgnoreContent, \FILE_APPEND); +} + +#[AsTask(description: 'Install Sylius')] +function sylius(): void +{ + $base = rtrim(variable('root_dir') . '/application'); + + $gitIgnore = $base . '/.gitignore'; + $gitIgnoreContent = ''; + if (file_exists($gitIgnore)) { + $gitIgnoreContent = file_get_contents($gitIgnore); + } + + build(); + docker_compose_run('composer create-project sylius/sylius-standard sylius'); + + fs()->mirror($base . '/sylius/', $base, options: ['override' => true]); + fs()->remove([$base . '/sylius', $base . '/var']); + + docker_compose_run("sed -i 's#^DATABASE_URL.*#DATABASE_URL=postgresql://app:app@postgres:5432/app\\?serverVersion=16\\&charset=utf8#' .env"); + file_put_contents($gitIgnore, $gitIgnoreContent, \FILE_APPEND); +} diff --git a/.castor/qa.php b/.castor/qa.php index 4fd8412..7dd1404 100644 --- a/.castor/qa.php +++ b/.castor/qa.php @@ -2,6 +2,8 @@ namespace qa; +use Castor\Attribute\AsOption; +use Castor\Attribute\AsRawTokens; use Castor\Attribute\AsTask; use function Castor\io; @@ -12,12 +14,12 @@ #[AsTask(description: 'Runs all QA tasks')] function all(): int { - install(); $cs = cs(); $phpstan = phpstan(); + $twigCs = twigCs(); $phpunit = phpunit(); - return max($cs, $phpstan, $phpunit); + return max($cs, $phpstan, $twigCs, $phpunit); } #[AsTask(description: 'Installs tooling')] @@ -27,31 +29,83 @@ function install(): void docker_compose_run('composer install -o', workDir: '/var/www/tools/php-cs-fixer'); docker_compose_run('composer install -o', workDir: '/var/www/tools/phpstan'); + docker_compose_run('composer install -o', workDir: '/var/www/tools/twig-cs-fixer'); } -#[AsTask(description: 'Update tooling')] +#[AsTask(description: 'Updates tooling')] function update(): void { - io()->title('Update QA tooling'); + io()->title('Updating QA tooling'); docker_compose_run('composer update -o', workDir: '/var/www/tools/php-cs-fixer'); docker_compose_run('composer update -o', workDir: '/var/www/tools/phpstan'); + docker_compose_run('composer update -o', workDir: '/var/www/tools/twig-cs-fixer'); } +/** + * @param string[] $rawTokens + */ #[AsTask(description: 'Runs PHPUnit', aliases: ['phpunit'])] -function phpunit(): int +function phpunit(#[AsRawTokens] array $rawTokens = []): int { - return docker_exit_code('bin/phpunit'); + if (!is_file(variable('root_dir') . '/bin/phpunit')) { + return 0; + } + + io()->section('Running PHPUnit...'); + + return docker_exit_code('bin/phpunit ' . implode(' ', $rawTokens)); } #[AsTask(description: 'Runs PHPStan', aliases: ['phpstan'])] -function phpstan(): int -{ +function phpstan( + #[AsOption(description: 'Generate baseline file', shortcut: 'b')] + bool $baseline = false, +): int { if (!is_dir(variable('root_dir') . '/tools/phpstan/vendor')) { install(); } - return docker_exit_code('phpstan', workDir: '/var/www'); + io()->section('Running PHPStan...'); + + $options = $baseline ? '--generate-baseline --allow-empty-baseline' : ''; + $command = \sprintf('phpstan analyse --memory-limit=-1 %s -v', $options); + + return docker_exit_code($command, workDir: '/var/www'); +} + +#[AsTask(description: 'Runs Security audit')] +function securityAudit(): int +{ + $basePath = \sprintf('%s/application', variable('root_dir')); + + if (is_file("{$basePath}/composer.lock")) { + io()->text('Running Composer audit...'); + + $exitCode = docker_exit_code('composer audit'); + + if (0 !== $exitCode) { + return $exitCode; + } + } + + if (is_file("{$basePath}/yarn.lock")) { + io()->text('Running Yarn audit...'); + + $exitCode = docker_exit_code('yarn audit'); + + if (0 !== $exitCode) { + return $exitCode; + } + } + + if (is_file("{$basePath}/package-lock.json")) { + io()->text('Running NPM audit...'); + + return docker_exit_code('npm audit'); + } + + return 0; } #[AsTask(description: 'Fixes Coding Style', aliases: ['cs'])] @@ -61,9 +115,27 @@ function cs(bool $dryRun = false): int install(); } + io()->section('Running PHP CS Fixer...'); + if ($dryRun) { return docker_exit_code('php-cs-fixer fix --dry-run --diff', workDir: '/var/www'); } - return docker_exit_code('php-cs-fixer fix', workDir: '/var/www'); + return docker_exit_code('php-cs-fixer fix -v', workDir: '/var/www'); +} + +#[AsTask(description: 'Fixes Twig Coding Style', aliases: ['twig-cs'])] +function twigCs(bool $dryRun = false): int +{ + if (!is_dir(variable('root_dir') . '/tools/twig-cs-fixer/vendor')) { + install(); + } + + io()->section('Running Twig CS Fixer...'); + + if ($dryRun) { + return docker_exit_code('twig-cs-fixer', workDir: '/var/www'); + } + + return docker_exit_code('twig-cs-fixer --fix', workDir: '/var/www'); } diff --git a/.env b/.env index 4cbe4f9..ffa3ca2 100644 --- a/.env +++ b/.env @@ -25,7 +25,7 @@ APP_SECRET=870686bf53a35819535f3c8158327e28 # # DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8&charset=utf8mb4" -DATABASE_URL="postgresql://monologue:monologue@postgres:5432/monologue?serverVersion=14&charset=utf8" +DATABASE_URL="postgresql://app:app@postgres:5432/app?serverVersion=14&charset=utf8" ###< doctrine/doctrine-bundle ### TIMEZONE=Europe/Paris diff --git a/.env.test b/.env.test index 7424138..9729c3b 100644 --- a/.env.test +++ b/.env.test @@ -1,7 +1,7 @@ # define your env variables for the test env here KERNEL_CLASS='App\Kernel' APP_SECRET='$ecretf0rt3st' -SYMFONY_DEPRECATIONS_HELPER=999999 +SYMFONY_DEPRECATIONS_HELPER=disabled SLACK_CHANNEL=MY_CHANNEL_ID SLACK_SIGNING_SECRET='' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76c5506..b50942f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Check php/Dockerfile uses: hadolint/hadolint-action@v3.1.0 @@ -41,10 +41,10 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: setup-castor - uses: castor-php/setup-castor@v0.1.0 + uses: castor-php/setup-castor@v1.0.0 - name: "Build and start the infrastructure" run: "castor start" @@ -52,8 +52,14 @@ jobs: - name: "Check PHP coding standards" run: "castor qa:cs --dry-run" + - name: "Run Twig CS" + run: "castor qa:twig-cs --dry-run" + - name: "Run PHPStan" - run: "castor qa:phpstan" + run: | + # PHPStan need the container to be compiled + castor --context=ci builder bin/console about --env=dev + castor qa:phpstan - name: "Run PHPUnit" run: "castor qa:phpunit" diff --git a/.gitignore b/.gitignore index 063f21f..d457d1f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -/node_modules - ###> symfony/framework-bundle ### /.env.local /.env.local.php @@ -16,8 +14,6 @@ .phpunit.result.cache /phpunit.xml ###< symfony/phpunit-bridge ### -/.serverless/ -/serverless.yml ###> friendsofphp/php-cs-fixer ### /.php-cs-fixer.cache @@ -25,6 +21,9 @@ ###> infrastructure ### /.castor.stub.php +/.home/ /infrastructure/docker/docker-compose.override.yml /infrastructure/docker/services/router/certs/*.pem ###< infrastructure ### + +.twig-cs-fixer.cache diff --git a/.home/.gitignore b/.home/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 01280c7..759e5fd 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -1,24 +1,29 @@ ignoreVCSIgnored(true) + ->ignoreDotFiles(false) ->in(__DIR__) - ->notPath('config/reference.php') - ->exclude('var') + ->append([ + __FILE__, + ]) ; -return (new PhpCsFixer\Config()) +return new PhpCsFixer\Config() + ->setUnsupportedPhpVersionAllowed(true) ->setRiskyAllowed(true) ->setRules([ - '@PHP81Migration' => true, + '@PHP8x5Migration' => true, '@PhpCsFixer' => true, + '@PhpCsFixer:risky' => true, '@Symfony' => true, - '@Symfony:risky' => true, 'php_unit_internal_class' => false, // From @PhpCsFixer but we don't want it 'php_unit_test_class_requires_covers' => false, // From @PhpCsFixer but we don't want it 'phpdoc_add_missing_param_annotation' => false, // From @PhpCsFixer but we don't want it 'concat_space' => ['spacing' => 'one'], 'ordered_class_elements' => true, // Symfony(PSR12) override the default value, but we don't want 'blank_line_before_statement' => true, // Symfony(PSR12) override the default value, but we don't want + 'declare_strict_types' => false, // PhpCsFixer:risky override the default value, but we don't want ]) ->setFinder($finder) ; diff --git a/castor.php b/castor.php index 19b5265..da1416c 100644 --- a/castor.php +++ b/castor.php @@ -2,6 +2,7 @@ use Castor\Attribute\AsTask; +use function Castor\context; use function Castor\guard_min_version; use function Castor\import; use function Castor\io; @@ -10,15 +11,17 @@ use function docker\about; use function docker\build; use function docker\docker_compose_run; -use function docker\generate_certificates; use function docker\up; +// use function docker\workers_start; +// use function docker\workers_stop; + guard_min_version('1.5.0'); import(__DIR__ . '/.castor'); /** - * @return array{project_name: string, root_domain: string, extra_domains: string[], php_version: string} + * @return array{project_name: string, root_domain: string} */ function create_default_variables(): array { @@ -28,10 +31,6 @@ function create_default_variables(): array return [ 'project_name' => $projectName, 'root_domain' => "{$projectName}.{$tld}", - 'extra_domains' => [ - "www.{$projectName}.{$tld}", - ], - 'php_version' => '8.3', ]; } @@ -40,13 +39,12 @@ function start(): void { io()->title('Starting the stack'); - generate_certificates(force: false); + // workers_stop(); build(); - up(profiles: ['default']); // We can't start worker now, they are not installed - cache_clear(); install(); + up(profiles: ['default']); // We can't start worker now, they are not installed migrate(); - migrate('test'); + // workers_start(); notify('The stack is now up and running.'); io()->success('The stack is now up and running.'); @@ -67,7 +65,7 @@ function install(): void } if (is_file("{$basePath}/yarn.lock")) { io()->section('Installing Node.js dependencies'); - docker_compose_run('yarn install --frozen-lockfile'); + docker_compose_run('yarn install --immutable'); } elseif (is_file("{$basePath}/package.json")) { io()->section('Installing Node.js dependencies'); @@ -85,31 +83,56 @@ function install(): void qa\install(); } -#[AsTask(description: 'Clear the application cache', namespace: 'app', aliases: ['cache-clear'])] -function cache_clear(): void +#[AsTask(description: 'Update dependencies')] +function update(bool $withTools = false): void +{ + io()->title('Updating dependencies...'); + + docker_compose_run('composer update -o'); + + if ($withTools) { + qa\update(); + } +} + +#[AsTask(description: 'Clears the application cache', namespace: 'app', aliases: ['cache-clear'])] +function cache_clear(bool $warm = true): void { io()->title('Clearing the application cache'); docker_compose_run('rm -rf var/cache/'); - // On the very first run, the vendor does not exist yet - if (is_dir(variable('root_dir') . '/vendor')) { - docker_compose_run('bin/console cache:warmup'); + + if ($warm) { + cache_warmup(); } } +#[AsTask(description: 'Warms the application cache', namespace: 'app', aliases: ['cache-warmup'])] +function cache_warmup(): void +{ + io()->title('Warming the application cache'); + + docker_compose_run('bin/console cache:warmup', c: context()->withAllowFailure()); +} + #[AsTask(description: 'Migrates database schema', namespace: 'app:db', aliases: ['migrate'])] -function migrate(string $env = 'dev'): void +function migrate(): void { io()->title('Migrating the database schema'); - docker_compose_run('bin/console doctrine:database:create --if-not-exists --env=' . $env); - docker_compose_run('bin/console doctrine:migration:migrate -n --allow-no-migration --all-or-nothing --env=' . $env); + docker_compose_run('bin/console doctrine:database:create --if-not-exists'); + docker_compose_run('bin/console doctrine:migration:migrate -n --allow-no-migration --all-or-nothing'); } -#[AsTask(description: 'Loads fixtures', namespace: 'app:db', aliases: ['fixture'])] +#[AsTask(description: 'Loads fixtures', namespace: 'app:db', aliases: ['fixtures'])] function fixtures(): void { - io()->title('Loads fixtures'); + io()->warning('This task is not implemented yet, you can use one of the following commands to load your fixtures:'); + + // io()->title('Loads fixtures'); - docker_compose_run('bin/console doctrine:fixture:load -n'); + // Uncomment one of them... + // docker_compose_run('bin/console doctrine:fixture:load -n'); + // docker_compose_run('bin/console foundry:load-fixtures -n'); + // docker_compose_run('bin/console sylius:fixture:load -n'); } diff --git a/foobar.sh b/foobar.sh new file mode 100644 index 0000000..ba49b40 --- /dev/null +++ b/foobar.sh @@ -0,0 +1,7 @@ +rm -rf tools/ infrastructure/ .castor/ .php-cs-fixer.php phpstan.neon +\cp -ra /home/gregoire/dev/github.com/jolicode/docker-starter/infrastructure/ . +\cp -ra /home/gregoire/dev/github.com/jolicode/docker-starter/.castor/ . +\cp -ra /home/gregoire/dev/github.com/jolicode/docker-starter/castor.php . +\cp -ra /home/gregoire/dev/github.com/jolicode/docker-starter/.php-cs-fixer.php . +\cp -ra /home/gregoire/dev/github.com/jolicode/docker-starter/phpstan.neon . +\cp -ra /home/gregoire/dev/github.com/jolicode/docker-starter/tools/ . diff --git a/infrastructure/docker/docker-compose.builder.yml b/infrastructure/docker/docker-compose.builder.yml deleted file mode 100644 index 1ad157f..0000000 --- a/infrastructure/docker/docker-compose.builder.yml +++ /dev/null @@ -1,30 +0,0 @@ -volumes: - builder-data: {} - -services: - builder: - build: - context: services/php - target: builder - # cache_to: - # - "ghcr.io/jolicode/monologue/builder:${BUILDER_VERSION:-latest}" - # cache_from: - # - "ghcr.io/jolicode/monologue/builder:${BUILDER_VERSION:-latest}" - init: true - depends_on: - - postgres - environment: - - COMPOSER_MEMORY_LIMIT=-1 - - UID=${USER_ID} - # The following list contains the common environment variables exposed by CI platforms - - GITHUB_ACTIONS - - CI # Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari - - CONTINUOUS_INTEGRATION # Travis CI, Cirrus CI - - BUILD_NUMBER # Jenkins, TeamCity - - RUN_ID # TaskCluster, dsari - volumes: - - "builder-data:/home/app" - - "${COMPOSER_CACHE_DIR}:/home/app/.composer/cache" - - "../..:/var/www:cached" - profiles: - - default diff --git a/infrastructure/docker/docker-compose.dev.yml b/infrastructure/docker/docker-compose.dev.yml new file mode 100644 index 0000000..20a12c7 --- /dev/null +++ b/infrastructure/docker/docker-compose.dev.yml @@ -0,0 +1,14 @@ +services: + router: + build: services/router + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + - "./services/router/certs:/etc/ssl/certs" + ports: + - "80:80" + - "443:443" + - "8080:8080" + networks: + - default + profiles: + - default diff --git a/infrastructure/docker/docker-compose.yml b/infrastructure/docker/docker-compose.yml index 73504b6..f601e97 100644 --- a/infrastructure/docker/docker-compose.yml +++ b/infrastructure/docker/docker-compose.yml @@ -1,23 +1,38 @@ +# Templates to factorize the service definitions +x-templates: + worker_base: &worker_base + build: + context: services/php + target: worker + user: "${USER_ID}:${USER_ID}" + environment: + - APP_ENV + depends_on: + postgres: + condition: service_healthy + volumes: + - "../..:/var/www:cached" + profiles: + - worker + volumes: postgres-data: {} + # # Needed if $XDG_ env vars have been overridden + # builder-yarn-data: {} services: - router: - build: - context: services/router - # cache_to: - # - "ghcr.io/jolicode/monologue/router:${ROUTER__VERSION:-latest}" - # cache_from: - # - "ghcr.io/jolicode/monologue/router:${ROUTER__VERSION:-latest}" + postgres: + image: postgres:16 + environment: + - POSTGRES_USER=app + - POSTGRES_PASSWORD=app volumes: - - "/var/run/docker.sock:/var/run/docker.sock" - - "./services/router/certs:/etc/ssl/certs" - ports: - - "80:80" - - "443:443" - - "8080:8080" - networks: - - default + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 profiles: - default @@ -25,15 +40,17 @@ services: build: context: services/php target: frontend - # cache_to: - # - "ghcr.io/jolicode/monologue/frontend:${FRONTEND_VERSION:-latest}" - # cache_from: - # - "ghcr.io/jolicode/monologue/frontend:${FRONTEND_VERSION:-latest}" + cache_from: + - "type=registry,ref=${REGISTRY:-}/frontend:cache" + user: "${USER_ID}:${USER_ID}" + environment: + - APP_ENV + volumes: + - "../..:/var/www:cached" + - "../../.home:/home/app:cached" depends_on: postgres: condition: service_healthy - volumes: - - "../..:/var/www:cached" profiles: - default labels: @@ -45,18 +62,34 @@ services: # Comment the next line to be able to access frontend via HTTP instead of HTTPS - "traefik.http.routers.${PROJECT_NAME}-frontend-unsecure.middlewares=redirect-to-https@file" - postgres: - image: postgres:16 + # worker_messenger: + # <<: *worker_base + # command: php -d memory_limit=1G /var/www/bin/console messenger:consume async --memory-limit=128M + + builder: + build: + context: services/php + target: builder + cache_from: + - "type=registry,ref=${REGISTRY:-}/builder:cache" + init: true + user: "${USER_ID}:${USER_ID}" environment: - POSTGRES_PASSWORD: monologue - POSTGRES_USER: monologue - POSTGRES_DB: monologue + - APP_ENV + # The following list contains the common environment variables exposed by CI platforms + - GITHUB_ACTIONS + - CI # Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari + - CONTINUOUS_INTEGRATION # Travis CI, Cirrus CI + - BUILD_NUMBER # Jenkins, TeamCity + - RUN_ID # TaskCluster, dsari volumes: - - postgres-data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 5s - timeout: 5s - retries: 5 + - "../..:/var/www:cached" + - "../../.home:/home/app:cached" + # Needed when $XDG_ env vars have overridden, to persist the yarn + # cache between builder and watcher, adapt according to the location + # of $XDG_DATA_HOME + # - "builder-yarn-data:/data/yarn" + depends_on: + - postgres profiles: - - default + - builder diff --git a/infrastructure/docker/services/php/Dockerfile b/infrastructure/docker/services/php/Dockerfile index 2979834..eeda124 100644 --- a/infrastructure/docker/services/php/Dockerfile +++ b/infrastructure/docker/services/php/Dockerfile @@ -1,7 +1,6 @@ # hadolint global ignore=DL3008 -FROM debian:12.6-slim AS php-base -LABEL org.opencontainers.image.source https://github.com/jolicode/monologue +FROM debian:13.5-slim AS php-base SHELL ["/bin/bash", "-o", "pipefail", "-c"] @@ -12,7 +11,7 @@ RUN apt-get update \ gnupg \ && curl -sSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb \ && dpkg -i /tmp/debsuryorg-archive-keyring.deb \ - && echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php bookworm main" > /etc/apt/sources.list.d/sury.list \ + && echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ trixie main" > /etc/apt/sources.list.d/sury.list \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* @@ -32,6 +31,7 @@ RUN apt-get update \ "php${PHP_VERSION}-cli" \ "php${PHP_VERSION}-common" \ "php${PHP_VERSION}-curl" \ + "php${PHP_VERSION}-gd" \ "php${PHP_VERSION}-iconv" \ "php${PHP_VERSION}-intl" \ "php${PHP_VERSION}-mbstring" \ @@ -42,24 +42,15 @@ RUN apt-get update \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* -# Fake user to maps with the one on the host -ARG USER_ID -COPY entrypoint / -RUN addgroup --gid $USER_ID app && \ - adduser --system --uid $USER_ID --home /home/app --shell /bin/bash app && \ - curl -Ls https://github.com/tianon/gosu/releases/download/1.17/gosu-amd64 | \ - install /dev/stdin /usr/local/bin/gosu && \ - sed "s/{{ application_user }}/app/g" -i /entrypoint - # Configuration COPY base/php-configuration /etc/php/${PHP_VERSION} ENV PHP_VERSION=${PHP_VERSION} +ENV HOME=/home/app +ENV COMPOSER_MEMORY_LIMIT=-1 WORKDIR /var/www -ENTRYPOINT [ "/entrypoint" ] - FROM php-base AS frontend RUN apt-get update \ @@ -75,7 +66,9 @@ RUN useradd -s /bin/false nginx COPY frontend/php-configuration /etc/php/${PHP_VERSION} COPY frontend/etc/nginx/. /etc/nginx/ +RUN rm -rf /etc/service/ COPY frontend/etc/service/. /etc/service/ +RUN chmod 777 /etc/service/*/supervise/ RUN phpenmod app-default \ && phpenmod app-fpm @@ -84,39 +77,54 @@ EXPOSE 80 CMD ["runsvdir", "-P", "/etc/service"] +FROM php-base AS worker + FROM php-base AS builder SHELL ["/bin/bash", "-o", "pipefail", "-c"] -ARG NODEJS_VERSION=20.x +ARG NODEJS_VERSION=24.x RUN curl -s https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /usr/share/keyrings/nodesource.gpg \ && echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODEJS_VERSION} nodistro main" > /etc/apt/sources.list.d/nodesource.list # Default toys +ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 RUN apt-get update \ && apt-get install -y --no-install-recommends \ - git \ - make \ - sudo \ - unzip \ + "git" \ + "make" \ + "nodejs" \ + "php${PHP_VERSION}-dev" \ + "sudo" \ + "unzip" \ && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* \ + && corepack enable \ + && yarn set version stable + +# Install a fake sudo command +# This is commented out by default because it exposes a security risk if you use this image in production, but it may be useful for development +# Use it at your own risk +# COPY base/sudo.sh /usr/local/bin/sudo +# RUN curl -L https://github.com/tianon/gosu/releases/download/1.16/gosu-amd64 -o /usr/local/bin/gosu && \ +# chmod u+s /usr/local/bin/gosu && \ +# chmod +x /usr/local/bin/gosu && \ +# chmod +x /usr/local/bin/sudo # Config -COPY builder/etc/. /etc/ COPY builder/php-configuration /etc/php/${PHP_VERSION} -RUN adduser app sudo \ - && mkdir /var/log/php \ - && chmod 777 /var/log/php \ - && phpenmod app-default \ +RUN phpenmod app-default \ && phpenmod app-builder # Composer -COPY --from=composer/composer:2.8.2 /usr/bin/composer /usr/bin/composer -RUN mkdir -p "/home/app/.composer/cache" \ - && chown app: /home/app/.composer -R +COPY --from=composer/composer:2.10.0 /usr/bin/composer /usr/bin/composer + +# Pie +RUN curl -L --output /usr/local/bin/pie https://github.com/php/pie/releases/download/1.4.5/pie.phar \ + && chmod +x /usr/local/bin/pie -ADD https://raw.githubusercontent.com/symfony/symfony/refs/heads/7.2/src/Symfony/Component/Console/Resources/completion.bash /tmp/completion.bash +# Autocompletion +ADD https://raw.githubusercontent.com/symfony/symfony/refs/heads/7.4/src/Symfony/Component/Console/Resources/completion.bash /tmp/completion.bash # Composer symfony/console version is too old, and doest not support "API version feature", so we remove it # Hey, while we are at it, let's add some more completion diff --git a/infrastructure/docker/services/php/base/sudo.sh b/infrastructure/docker/services/php/base/sudo.sh new file mode 100644 index 0000000..cdbf17a --- /dev/null +++ b/infrastructure/docker/services/php/base/sudo.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +export GOSU_PLEASE_LET_ME_BE_COMPLETELY_INSECURE_I_GET_TO_KEEP_ALL_THE_PIECES="I've seen things you people wouldn't believe. Attack ships on fire off the shoulder of Orion. I watched C-beams glitter in the dark near the Tannhäuser Gate. All those moments will be lost in time, like tears in rain. Time to die." +exec gosu 0:0 $@ diff --git a/infrastructure/docker/services/php/builder/etc/sudoers.d/sudo b/infrastructure/docker/services/php/builder/etc/sudoers.d/sudo deleted file mode 100644 index b835421..0000000 --- a/infrastructure/docker/services/php/builder/etc/sudoers.d/sudo +++ /dev/null @@ -1 +0,0 @@ -%sudo ALL=(ALL) NOPASSWD: ALL diff --git a/infrastructure/docker/services/php/entrypoint b/infrastructure/docker/services/php/entrypoint deleted file mode 100755 index 1f15e84..0000000 --- a/infrastructure/docker/services/php/entrypoint +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh - -set -e -set -u - -if [ $(id -u) != 0 ]; then - echo "Running this image as non root is not allowed" - exit 1 -fi - -: "${UID:=0}" -: "${GID:=${UID}}" - -if [ "$#" = 0 ]; then - set -- "$(command -v bash 2>/dev/null || command -v sh)" -l -fi - -if [ "$UID" != 0 ]; then - usermod -u "$UID" "{{ application_user }}" >/dev/null 2>/dev/null && { - groupmod -g "$GID" "{{ application_user }}" >/dev/null 2>/dev/null || - usermod -a -G "$GID" "{{ application_user }}" >/dev/null 2>/dev/null - } - set -- gosu "${UID}:${GID}" "${@}" -fi - -exec "$@" diff --git a/infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf b/infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf index 8e939f7..42be9e2 100644 --- a/infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf +++ b/infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf @@ -1,5 +1,5 @@ user nginx; -pid /var/run/nginx.pid; +pid /tmp/nginx.pid; daemon off; error_log /proc/self/fd/2; include /etc/nginx/modules-enabled/*.conf; @@ -25,6 +25,12 @@ http { gzip_http_version 1.1; gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; + client_body_temp_path /tmp/nginx-client_body_temp_path; + fastcgi_temp_path /tmp/nginx-fastcgi_temp_path; + proxy_temp_path /tmp/nginx-proxy_temp_path; + scgi_temp_path /tmp/nginx-scgi_temp_path; + uwsgi_temp_path /tmp/nginx-uwsgi_temp_path; + server { listen 0.0.0.0:80; root /var/www/public; diff --git a/infrastructure/docker/services/php/frontend/etc/service/nginx/supervise/.gitignore b/infrastructure/docker/services/php/frontend/etc/service/nginx/supervise/.gitignore new file mode 100644 index 0000000..78d9101 --- /dev/null +++ b/infrastructure/docker/services/php/frontend/etc/service/nginx/supervise/.gitignore @@ -0,0 +1,2 @@ +/* +!.gitignore diff --git a/infrastructure/docker/services/php/frontend/etc/service/php-fpm/supervise/.gitignore b/infrastructure/docker/services/php/frontend/etc/service/php-fpm/supervise/.gitignore new file mode 100644 index 0000000..78d9101 --- /dev/null +++ b/infrastructure/docker/services/php/frontend/etc/service/php-fpm/supervise/.gitignore @@ -0,0 +1,2 @@ +/* +!.gitignore diff --git a/infrastructure/docker/services/php/frontend/php-configuration/fpm/php-fpm.conf b/infrastructure/docker/services/php/frontend/php-configuration/fpm/php-fpm.conf index 38b5901..3b74d34 100644 --- a/infrastructure/docker/services/php/frontend/php-configuration/fpm/php-fpm.conf +++ b/infrastructure/docker/services/php/frontend/php-configuration/fpm/php-fpm.conf @@ -1,11 +1,8 @@ [global] -pid = /var/run/php-fpm.pid error_log = /proc/self/fd/2 daemonize = no [www] -user = app -group = app listen = 127.0.0.1:9000 pm = dynamic pm.max_children = 25 diff --git a/infrastructure/docker/services/router/Dockerfile b/infrastructure/docker/services/router/Dockerfile index 835cebd..9d52359 100644 --- a/infrastructure/docker/services/router/Dockerfile +++ b/infrastructure/docker/services/router/Dockerfile @@ -1,4 +1,4 @@ -FROM traefik:v3.1 +FROM traefik:v3.7.1 COPY traefik /etc/traefik diff --git a/infrastructure/docker/services/router/traefik/traefik.yaml b/infrastructure/docker/services/router/traefik/traefik.yaml index bd66302..b7be9f2 100644 --- a/infrastructure/docker/services/router/traefik/traefik.yaml +++ b/infrastructure/docker/services/router/traefik/traefik.yaml @@ -22,8 +22,8 @@ api: entryPoints: http: - address: ":80" + address: ":80" https: - address: ":443" + address: ":443" traefik: # this one exists by default - address: ":8080" + address: ":8080" diff --git a/phpstan.neon b/phpstan.neon index c4f70ed..2ab0f84 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,6 @@ includes: - phpstan-baseline.neon + parameters: level: 8 paths: @@ -9,15 +10,15 @@ parameters: - .castor/ scanFiles: - .castor.stub.php + # scanDirectories: + # - vendor tmpDir: tools/phpstan/var inferPrivatePropertyTypeFromConstructor: true ignoreErrors: - - '{Property .* type has no value type specified in iterable type array\.}' - - '{Method .* return type has no value type specified in iterable type array\.}' - - '{Method .* has parameter .* with no value type specified in iterable type array\.}' + - identifier: missingType.iterableValue symfony: - container_xml_path: 'var/cache/dev/App_KernelDevDebugContainer.xml' + containerXmlPath: 'var/cache/dev/App_KernelDevDebugContainer.xml' typeAliases: ContextData: ''' @@ -25,13 +26,13 @@ parameters: project_name: string, root_domain: string, extra_domains: string[], - project_directory: string, php_version: string, - docker_compose_files: string[], + docker_compose_files: list, + docker_compose_run_environment: list, macos: bool, power_shell: bool, user_id: int, root_dir: string, - composer_cache_dir: string, + registry?: ?string, } ''' diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b638e8d..0891198 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -16,7 +16,7 @@ - + diff --git a/public/index.php b/public/index.php index 7fbc8cf..ea9912e 100644 --- a/public/index.php +++ b/public/index.php @@ -4,6 +4,6 @@ require_once dirname(__DIR__) . '/vendor/autoload_runtime.php'; -return function (array $context) { +return static function (array $context) { return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); }; diff --git a/src/Controller/HomepageController.php b/src/Controller/HomepageController.php index 4cc6a1a..df8f128 100644 --- a/src/Controller/HomepageController.php +++ b/src/Controller/HomepageController.php @@ -7,7 +7,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; class HomepageController extends AbstractController { diff --git a/src/Controller/SlackController.php b/src/Controller/SlackController.php index 4a03bee..06c1069 100644 --- a/src/Controller/SlackController.php +++ b/src/Controller/SlackController.php @@ -14,7 +14,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; #[Route('', defaults: ['slack' => true])] class SlackController extends AbstractController @@ -45,7 +45,7 @@ public function messages(Request $request): Response public function action(Request $request): Response { try { - $payload = json_decode((string) $request->request->get('payload'), true, 512, \JSON_THROW_ON_ERROR); + $payload = json_decode((string) $request->request->get('payload'), true, 512, JSON_THROW_ON_ERROR); } catch (\JsonException) { return new Response('No payload', 400); } diff --git a/src/Doctrine/Type/DateTimeImmutableWithMillis.php b/src/Doctrine/Type/DateTimeImmutableWithMillis.php index 8742a33..00b75ac 100644 --- a/src/Doctrine/Type/DateTimeImmutableWithMillis.php +++ b/src/Doctrine/Type/DateTimeImmutableWithMillis.php @@ -28,7 +28,7 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform): mixe return $value->format('Y-m-d H:i:s.v'); } - throw new ConversionException(sprintf('Expected \DateTimeInterface or null, got %s', get_debug_type($value))); + throw new ConversionException(\sprintf('Expected \DateTimeInterface or null, got %s', get_debug_type($value))); } public function convertToPHPValue($value, AbstractPlatform $platform): \DateTimeInterface|\DateTimeImmutable|null @@ -44,7 +44,7 @@ public function convertToPHPValue($value, AbstractPlatform $platform): \DateTime } if (!$val) { - throw new ConversionException(sprintf('Could not convert database value "%s" to %s', get_debug_type($value), $this->getName())); + throw new ConversionException(\sprintf('Could not convert database value "%s" to %s', get_debug_type($value), $this->getName())); } return $val; diff --git a/src/EventSubscriber/ChallengeSubscriber.php b/src/EventSubscriber/ChallengeSubscriber.php index 23f2680..932f568 100644 --- a/src/EventSubscriber/ChallengeSubscriber.php +++ b/src/EventSubscriber/ChallengeSubscriber.php @@ -15,7 +15,7 @@ public function verifyChallenge(RequestEvent $event): void } try { - $payload = json_decode($event->getRequest()->getContent(), true, 512, \JSON_THROW_ON_ERROR); + $payload = json_decode($event->getRequest()->getContent(), true, 512, JSON_THROW_ON_ERROR); } catch (\JsonException) { return; } diff --git a/src/Slack/DebtListBlockBuilder.php b/src/Slack/DebtListBlockBuilder.php index f2fae4f..69ff06f 100644 --- a/src/Slack/DebtListBlockBuilder.php +++ b/src/Slack/DebtListBlockBuilder.php @@ -46,7 +46,7 @@ public function buildBlocks(string $usedId): array 'type' => 'section', 'text' => [ 'type' => 'mrkdwn', - 'text' => \sprintf('<@%s>, %s days ago.', $event->getAuthor(), (new \DateTime())->diff($event->getCreatedAt())->format('%a')), + 'text' => \sprintf('<@%s>, %s days ago.', $event->getAuthor(), new \DateTime()->diff($event->getCreatedAt())->format('%a')), ], ]; diff --git a/tests/Acceptance/AcceptenceTest.php b/tests/Acceptance/AcceptenceTest.php index 84319f8..7a5db05 100644 --- a/tests/Acceptance/AcceptenceTest.php +++ b/tests/Acceptance/AcceptenceTest.php @@ -21,7 +21,7 @@ protected function setUp(): void self::ensureKernelShutdown(); } - public function testWithAck() + public function testWithAck(): void { $client = self::createClient(); // We want to be able to mock some response @@ -33,17 +33,17 @@ public function testWithAck() $client->request('POST', '/message', content: $this->getFixtures('001_message_user_A')); - $this->assertSame(200, $client->getResponse()->getStatusCode()); - $this->assertSame(1, $this->conn->fetchOne('SELECT COUNT(*) FROM event')); - $this->assertSame(0, $this->conn->fetchOne('SELECT COUNT(*) FROM debt')); + self::assertSame(200, $client->getResponse()->getStatusCode()); + self::assertSame(1, $this->conn->fetchOne('SELECT COUNT(*) FROM event')); + self::assertSame(0, $this->conn->fetchOne('SELECT COUNT(*) FROM debt')); // #2 message from user A $client->request('POST', '/message', content: $this->getFixtures('002_message_user_A')); - $this->assertSame(200, $client->getResponse()->getStatusCode()); - $this->assertSame(2, $this->conn->fetchOne('SELECT COUNT(*) FROM event')); - $this->assertSame(0, $this->conn->fetchOne('SELECT COUNT(*) FROM debt')); + self::assertSame(200, $client->getResponse()->getStatusCode()); + self::assertSame(2, $this->conn->fetchOne('SELECT COUNT(*) FROM event')); + self::assertSame(0, $this->conn->fetchOne('SELECT COUNT(*) FROM debt')); // #1 message from user B @@ -56,9 +56,9 @@ public function testWithAck() }); $client->request('POST', '/message', content: $this->getFixtures('003_message_user_B')); - $this->assertSame(200, $client->getResponse()->getStatusCode()); - $this->assertSame(3, $this->conn->fetchOne('SELECT COUNT(*) FROM event')); - $this->assertSame(1, $this->conn->fetchOne('SELECT COUNT(*) FROM debt')); + self::assertSame(200, $client->getResponse()->getStatusCode()); + self::assertSame(3, $this->conn->fetchOne('SELECT COUNT(*) FROM event')); + self::assertSame(1, $this->conn->fetchOne('SELECT COUNT(*) FROM debt')); // /monologue from user A @@ -66,11 +66,11 @@ public function testWithAck() 'user_id' => 'U0FLDV6UW', ]); - $this->assertSame(200, $client->getResponse()->getStatusCode()); + self::assertSame(200, $client->getResponse()->getStatusCode()); $responseDecoded = json_decode($client->getResponse()->getContent(), true); - $this->assertCount(3, $responseDecoded['blocks']); - $this->assertSame('*Pending debts*', $responseDecoded['blocks'][0]['text']['text']); - $this->assertArrayHasKey('accessory', $responseDecoded['blocks'][2]); + self::assertCount(3, $responseDecoded['blocks']); + self::assertSame('*Pending debts*', $responseDecoded['blocks'][0]['text']['text']); + self::assertArrayHasKey('accessory', $responseDecoded['blocks'][2]); // /monologue from user B @@ -78,11 +78,11 @@ public function testWithAck() 'user_id' => 'UMYK1MQ3E', ]); - $this->assertSame(200, $client->getResponse()->getStatusCode()); + self::assertSame(200, $client->getResponse()->getStatusCode()); $responseDecoded = json_decode($client->getResponse()->getContent(), true); - $this->assertCount(3, $responseDecoded['blocks']); - $this->assertSame('*Pending debts*', $responseDecoded['blocks'][0]['text']['text']); - $this->assertFalse(\array_key_exists('accessory', $responseDecoded['blocks'][2])); + self::assertCount(3, $responseDecoded['blocks']); + self::assertSame('*Pending debts*', $responseDecoded['blocks'][0]['text']['text']); + self::assertFalse(\array_key_exists('accessory', $responseDecoded['blocks'][2])); // user A marks debt for user B as paid @@ -108,7 +108,7 @@ function (string $method, string $url, array $options = []): MockResponse { 'payload' => str_replace('DEBT_ID', $debId, $this->getFixtures('004_mark_as_paid')), ]); - $this->assertSame(200, $client->getResponse()->getStatusCode()); + self::assertSame(200, $client->getResponse()->getStatusCode()); // /monologue from user B @@ -116,13 +116,13 @@ function (string $method, string $url, array $options = []): MockResponse { 'user_id' => 'UMYK1MQ3E', ]); - $this->assertSame(200, $client->getResponse()->getStatusCode()); + self::assertSame(200, $client->getResponse()->getStatusCode()); $responseDecoded = json_decode($client->getResponse()->getContent(), true); - $this->assertCount(1, $responseDecoded['blocks']); - $this->assertSame('*There are no more debts*', $responseDecoded['blocks'][0]['text']['text']); + self::assertCount(1, $responseDecoded['blocks']); + self::assertSame('*There are no more debts*', $responseDecoded['blocks'][0]['text']['text']); } - public function testWithAmnesty() + public function testWithAmnesty(): void { $client = self::createClient(); // We want to be able to mock some response @@ -134,14 +134,14 @@ public function testWithAmnesty() $client->request('POST', '/message', content: $this->getFixtures('001_message_user_A')); - $this->assertSame(200, $client->getResponse()->getStatusCode()); + self::assertSame(200, $client->getResponse()->getStatusCode()); // #1 message from user B $mockHttpClient->setResponseFactory(new MockResponse('{"ok": true}')); $client->request('POST', '/message', content: $this->getFixtures('003_message_user_B')); - $this->assertSame(200, $client->getResponse()->getStatusCode()); + self::assertSame(200, $client->getResponse()->getStatusCode()); // /amnesty from user A @@ -149,8 +149,8 @@ public function testWithAmnesty() 'user_id' => 'U0FLDV6UW', ]); - $this->assertSame(200, $client->getResponse()->getStatusCode()); - $this->assertSame('{"text":"More people need to ask for amnesty to complete it! (1\/2)"}', $client->getResponse()->getContent()); + self::assertSame(200, $client->getResponse()->getStatusCode()); + self::assertSame('{"text":"More people need to ask for amnesty to complete it! (1\/2)"}', $client->getResponse()->getContent()); // /amnesty from user B @@ -166,8 +166,8 @@ public function testWithAmnesty() 'user_id' => 'UMYK1MQ3E', ]); - $this->assertSame(200, $client->getResponse()->getStatusCode()); - $this->assertSame('', $client->getResponse()->getContent()); + self::assertSame(200, $client->getResponse()->getStatusCode()); + self::assertSame('', $client->getResponse()->getContent()); // /monologue from user B @@ -175,10 +175,10 @@ public function testWithAmnesty() 'user_id' => 'UMYK1MQ3E', ]); - $this->assertSame(200, $client->getResponse()->getStatusCode()); + self::assertSame(200, $client->getResponse()->getStatusCode()); $responseDecoded = json_decode($client->getResponse()->getContent(), true); - $this->assertCount(1, $responseDecoded['blocks']); - $this->assertSame('*There are no more debts*', $responseDecoded['blocks'][0]['text']['text']); + self::assertCount(1, $responseDecoded['blocks']); + self::assertSame('*There are no more debts*', $responseDecoded['blocks'][0]['text']['text']); } private function getFixtures(string $name): string diff --git a/tests/Integration/ControlTower/DebtCreatorTest.php b/tests/Integration/ControlTower/DebtCreatorTest.php index 94ca7ad..08ed102 100644 --- a/tests/Integration/ControlTower/DebtCreatorTest.php +++ b/tests/Integration/ControlTower/DebtCreatorTest.php @@ -20,7 +20,7 @@ protected function setUp(): void ; } - public function testControlFirstMessage() + public function testControlFirstMessage(): void { // Author: foobar @@ -47,7 +47,7 @@ public function testControlFirstMessage() ], ]; - $this->assertNull($this->bigBrowser->createDebtIfNeeded($payload)); + self::assertNull($this->bigBrowser->createDebtIfNeeded($payload)); // Author: foobar, a bit later @@ -74,7 +74,7 @@ public function testControlFirstMessage() ], ]; - $this->assertNull($this->bigBrowser->createDebtIfNeeded($payload)); + self::assertNull($this->bigBrowser->createDebtIfNeeded($payload)); // Author: baz @@ -101,7 +101,7 @@ public function testControlFirstMessage() ], ]; - $this->assertInstanceOf(Debt::class, $this->bigBrowser->createDebtIfNeeded($payload)); + self::assertInstanceOf(Debt::class, $this->bigBrowser->createDebtIfNeeded($payload)); // Author: baz, a bit later @@ -128,7 +128,7 @@ public function testControlFirstMessage() ], ]; - $this->assertNull($this->bigBrowser->createDebtIfNeeded($payload)); + self::assertNull($this->bigBrowser->createDebtIfNeeded($payload)); // Author: foobar, a bit later, again @@ -155,7 +155,7 @@ public function testControlFirstMessage() ], ]; - $this->assertNull($this->bigBrowser->createDebtIfNeeded($payload)); + self::assertNull($this->bigBrowser->createDebtIfNeeded($payload)); // Author: foo, a bit later @@ -182,7 +182,7 @@ public function testControlFirstMessage() ], ]; - $this->assertInstanceOf(Debt::class, $this->bigBrowser->createDebtIfNeeded($payload)); + self::assertInstanceOf(Debt::class, $this->bigBrowser->createDebtIfNeeded($payload)); // Reaction, author Jean, later @@ -209,7 +209,7 @@ public function testControlFirstMessage() ], ]; - $this->assertInstanceOf(Debt::class, $this->bigBrowser->createDebtIfNeeded($payload)); + self::assertInstanceOf(Debt::class, $this->bigBrowser->createDebtIfNeeded($payload)); // Reaction, author Jean, later @@ -236,6 +236,6 @@ public function testControlFirstMessage() ], ]; - $this->assertNull($this->bigBrowser->createDebtIfNeeded($payload)); + self::assertNull($this->bigBrowser->createDebtIfNeeded($payload)); } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 5160486..8b7a9de 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -7,5 +7,5 @@ if (file_exists(dirname(__DIR__) . '/config/bootstrap.php')) { require dirname(__DIR__) . '/config/bootstrap.php'; } elseif (method_exists(Dotenv::class, 'bootEnv')) { - (new Dotenv())->bootEnv(dirname(__DIR__) . '/.env'); + new Dotenv()->bootEnv(dirname(__DIR__) . '/.env'); } diff --git a/tools/bin/twig-cs-fixer b/tools/bin/twig-cs-fixer new file mode 120000 index 0000000..9d588f2 --- /dev/null +++ b/tools/bin/twig-cs-fixer @@ -0,0 +1 @@ +../twig-cs-fixer/vendor/bin/twig-cs-fixer \ No newline at end of file diff --git a/tools/php-cs-fixer/composer.json b/tools/php-cs-fixer/composer.json index a7f67d5..03a50e3 100644 --- a/tools/php-cs-fixer/composer.json +++ b/tools/php-cs-fixer/composer.json @@ -1,11 +1,11 @@ { "type": "project", "require": { - "friendsofphp/php-cs-fixer": "^3.64.0" + "friendsofphp/php-cs-fixer": "^3.95.2" }, "config": { "platform": { - "php": "8.1" + "php": "8.3" }, "bump-after-update": true, "sort-packages": true diff --git a/tools/php-cs-fixer/composer.lock b/tools/php-cs-fixer/composer.lock index 8dfb676..758ab37 100644 --- a/tools/php-cs-fixer/composer.lock +++ b/tools/php-cs-fixer/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7f2b0362a40dad2f33333dcca9c01780", + "content-hash": "b77285aa27a9af2ea2fd8f7651887d77", "packages": [ { "name": "clue/ndjson-react", @@ -97,13 +97,13 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - }, "phpstan": { "includes": [ "extension.neon" ] + }, + "branch-alias": { + "dev-main": "3.x-dev" } }, "autoload": { @@ -151,16 +151,16 @@ }, { "name": "composer/semver", - "version": "3.4.3", + "version": "3.4.4", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", "shasum": "" }, "require": { @@ -212,7 +212,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.3" + "source": "https://github.com/composer/semver/tree/3.4.4" }, "funding": [ { @@ -222,13 +222,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-09-19T14:15:21+00:00" + "time": "2025-08-20T19:15:30+00:00" }, { "name": "composer/xdebug-handler", @@ -296,6 +292,75 @@ ], "time": "2024-05-06T16:37:16+00:00" }, + { + "name": "ergebnis/agent-detector", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/agent-detector.git", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/agent-detector/zipball/e211f17928c8b95a51e06040792d57f5462fb271", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271", + "shasum": "" + }, + "require": { + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.51.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.60.2", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.24.0", + "ergebnis/rector-rules": "^1.18.1", + "fakerphp/faker": "^1.24.1", + "infection/infection": "^0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.54", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "phpunit/phpunit": "^9.6.34", + "rector/rector": "^2.4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\AgentDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a detector for detecting the presence of an agent.", + "homepage": "https://github.com/ergebnis/agent-detector", + "support": { + "issues": "https://github.com/ergebnis/agent-detector/issues", + "security": "https://github.com/ergebnis/agent-detector/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/agent-detector" + }, + "time": "2026-05-07T08:19:07+00:00" + }, { "name": "evenement/evenement", "version": "v3.0.2", @@ -345,16 +410,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "1.2.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "8520451a140d3f46ac33042715115e290cf5785f" + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", - "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", "shasum": "" }, "require": { @@ -364,10 +429,10 @@ "fidry/makefile": "^0.2.0", "fidry/php-cs-fixer-config": "^1.1.2", "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^1.9.2", - "phpstan/phpstan-deprecation-rules": "^1.0.0", - "phpstan/phpstan-phpunit": "^1.2.2", - "phpstan/phpstan-strict-rules": "^1.4.4", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^8.5.31 || ^9.5.26", "webmozarts/strict-phpunit": "^7.5" }, @@ -394,7 +459,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" }, "funding": [ { @@ -402,61 +467,63 @@ "type": "github" } ], - "time": "2024-08-06T10:04:20+00:00" + "time": "2025-08-14T07:29:31+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.64.0", + "version": "v3.95.2", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "58dd9c931c785a79739310aef5178928305ffa67" + "reference": "a28d88a5e172b27e78d0816992b15a9df3da20f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/58dd9c931c785a79739310aef5178928305ffa67", - "reference": "58dd9c931c785a79739310aef5178928305ffa67", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/a28d88a5e172b27e78d0816992b15a9df3da20f1", + "reference": "a28d88a5e172b27e78d0816992b15a9df3da20f1", "shasum": "" }, "require": { - "clue/ndjson-react": "^1.0", + "clue/ndjson-react": "^1.3", "composer/semver": "^3.4", - "composer/xdebug-handler": "^3.0.3", + "composer/xdebug-handler": "^3.0.5", + "ergebnis/agent-detector": "^1.1.1", "ext-filter": "*", + "ext-hash": "*", "ext-json": "*", "ext-tokenizer": "*", - "fidry/cpu-core-counter": "^1.0", + "fidry/cpu-core-counter": "^1.3", "php": "^7.4 || ^8.0", - "react/child-process": "^0.6.5", - "react/event-loop": "^1.0", - "react/promise": "^2.0 || ^3.0", - "react/socket": "^1.0", - "react/stream": "^1.0", - "sebastian/diff": "^4.0 || ^5.0 || ^6.0", - "symfony/console": "^5.4 || ^6.0 || ^7.0", - "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", - "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", - "symfony/finder": "^5.4 || ^6.0 || ^7.0", - "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0", - "symfony/polyfill-mbstring": "^1.28", - "symfony/polyfill-php80": "^1.28", - "symfony/polyfill-php81": "^1.28", - "symfony/process": "^5.4 || ^6.0 || ^7.0", - "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" + "react/child-process": "^0.6.6", + "react/event-loop": "^1.5", + "react/socket": "^1.16", + "react/stream": "^1.4", + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0", + "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.33", + "symfony/polyfill-php80": "^1.33", + "symfony/polyfill-php81": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2 || ^8.0", + "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" }, "require-dev": { - "facile-it/paraunit": "^1.3 || ^2.3", - "infection/infection": "^0.29.5", - "justinrainbow/json-schema": "^5.2", - "keradus/cli-executor": "^2.1", - "mikey179/vfsstream": "^1.6.11", - "php-coveralls/php-coveralls": "^2.7", - "php-cs-fixer/accessible-object": "^1.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5", - "phpunit/phpunit": "^9.6.19 || ^10.5.21 || ^11.2", - "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0", - "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + "facile-it/paraunit": "^1.3.1 || ^2.11.0", + "infection/infection": "^0.32.7", + "justinrainbow/json-schema": "^6.8.0", + "keradus/cli-executor": "^2.3", + "mikey179/vfsstream": "^1.6.12", + "php-coveralls/php-coveralls": "^2.9.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.8", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.8", + "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.55", + "symfony/polyfill-php85": "^1.33", + "symfony/var-dumper": "^5.4.48 || ^6.4.32 || ^7.4.4 || ^8.0.8", + "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.8" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -471,7 +538,7 @@ "PhpCsFixer\\": "src/" }, "exclude-from-classmap": [ - "src/Fixer/Internal/*" + "src/**/Internal/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -497,7 +564,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.64.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.2" }, "funding": [ { @@ -505,7 +572,7 @@ "type": "github" } ], - "time": "2024-08-30T23:09:38+00:00" + "time": "2026-05-15T09:20:44+00:00" }, { "name": "psr/container", @@ -734,33 +801,33 @@ }, { "name": "react/child-process", - "version": "v0.6.5", + "version": "v0.6.7", "source": { "type": "git", "url": "https://github.com/reactphp/child-process.git", - "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43" + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/child-process/zipball/e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", - "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3", "shasum": "" }, "require": { "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "php": ">=5.3.0", "react/event-loop": "^1.2", - "react/stream": "^1.2" + "react/stream": "^1.4" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", - "react/socket": "^1.8", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" }, "type": "library", "autoload": { "psr-4": { - "React\\ChildProcess\\": "src" + "React\\ChildProcess\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -797,32 +864,28 @@ ], "support": { "issues": "https://github.com/reactphp/child-process/issues", - "source": "https://github.com/reactphp/child-process/tree/v0.6.5" + "source": "https://github.com/reactphp/child-process/tree/v0.6.7" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2022-09-16T13:41:56+00:00" + "time": "2025-12-23T15:25:20+00:00" }, { "name": "react/dns", - "version": "v1.13.0", + "version": "v1.14.0", "source": { "type": "git", "url": "https://github.com/reactphp/dns.git", - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", "shasum": "" }, "require": { @@ -877,7 +940,7 @@ ], "support": { "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.13.0" + "source": "https://github.com/reactphp/dns/tree/v1.14.0" }, "funding": [ { @@ -885,20 +948,20 @@ "type": "open_collective" } ], - "time": "2024-06-13T14:18:03+00:00" + "time": "2025-11-18T19:34:28+00:00" }, { "name": "react/event-loop", - "version": "v1.5.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/reactphp/event-loop.git", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", "shasum": "" }, "require": { @@ -949,7 +1012,7 @@ ], "support": { "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" }, "funding": [ { @@ -957,27 +1020,27 @@ "type": "open_collective" } ], - "time": "2023-11-13T13:48:05+00:00" + "time": "2025-11-17T20:46:25+00:00" }, { "name": "react/promise", - "version": "v3.2.0", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", "shasum": "" }, "require": { "php": ">=7.1.0" }, "require-dev": { - "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpstan/phpstan": "1.12.28 || 1.4.10", "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", @@ -1022,7 +1085,7 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.2.0" + "source": "https://github.com/reactphp/promise/tree/v3.3.0" }, "funding": [ { @@ -1030,20 +1093,20 @@ "type": "open_collective" } ], - "time": "2024-05-24T10:39:05+00:00" + "time": "2025-08-19T18:57:03+00:00" }, { "name": "react/socket", - "version": "v1.16.0", + "version": "v1.17.0", "source": { "type": "git", "url": "https://github.com/reactphp/socket.git", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", "shasum": "" }, "require": { @@ -1102,7 +1165,7 @@ ], "support": { "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.16.0" + "source": "https://github.com/reactphp/socket/tree/v1.17.0" }, "funding": [ { @@ -1110,7 +1173,7 @@ "type": "open_collective" } ], - "time": "2024-07-26T10:38:09+00:00" + "time": "2025-11-19T20:47:34+00:00" }, { "name": "react/stream", @@ -1192,29 +1255,29 @@ }, { "name": "sebastian/diff", - "version": "5.1.1", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", - "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^10.0", - "symfony/process": "^6.4" + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.1-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -1247,7 +1310,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" }, "funding": [ { @@ -1255,51 +1318,51 @@ "type": "github" } ], - "time": "2024-03-02T07:15:17+00:00" + "time": "2025-02-07T04:55:46+00:00" }, { "name": "symfony/console", - "version": "v6.4.15", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "f1fc6f47283e27336e7cebb9e8946c8de7bff9bd" + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/f1fc6f47283e27336e7cebb9e8946c8de7bff9bd", - "reference": "f1fc6f47283e27336e7cebb9e8946c8de7bff9bd", + "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0|^7.0" + "symfony/string": "^7.2|^8.0" }, "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -1333,7 +1396,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.15" + "source": "https://github.com/symfony/console/tree/v7.4.13" }, "funding": [ { @@ -1344,25 +1407,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-06T14:19:14+00:00" + "time": "2026-05-24T08:56:14+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -1370,12 +1437,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" } }, "autoload": { @@ -1400,7 +1467,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -1411,33 +1478,37 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.4.13", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e" + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", - "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e4a2e29753c7801f7a8340e066cfa788f3bc8101", + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<5.4", + "symfony/dependency-injection": "<6.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -1446,13 +1517,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/error-handler": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^5.4|^6.0|^7.0" + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -1480,7 +1552,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.13" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.9" }, "funding": [ { @@ -1491,25 +1563,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:18:03+00:00" + "time": "2026-04-18T13:18:21+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", "shasum": "" }, "require": { @@ -1518,12 +1594,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" } }, "autoload": { @@ -1556,7 +1632,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" }, "funding": [ { @@ -1567,34 +1643,38 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2026-01-05T13:30:16+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.13", + "version": "v7.4.11", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3" + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/4856c9cf585d5a0313d8d35afd681a526f038dd3", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^5.4|^6.4|^7.0" + "symfony/process": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -1622,7 +1702,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.13" + "source": "https://github.com/symfony/filesystem/tree/v7.4.11" }, "funding": [ { @@ -1633,32 +1713,36 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-25T15:07:50+00:00" + "time": "2026-05-11T16:38:44+00:00" }, { "name": "symfony/finder", - "version": "v6.4.13", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "daea9eca0b08d0ed1dc9ab702a46128fd1be4958" + "reference": "e0be088d22278583a82da281886e8c3592fbf149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/daea9eca0b08d0ed1dc9ab702a46128fd1be4958", - "reference": "daea9eca0b08d0ed1dc9ab702a46128fd1be4958", + "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", + "reference": "e0be088d22278583a82da281886e8c3592fbf149", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.0|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -1686,7 +1770,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.13" + "source": "https://github.com/symfony/finder/tree/v7.4.8" }, "funding": [ { @@ -1697,29 +1781,33 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-01T08:30:56+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/options-resolver", - "version": "v6.4.13", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "0a62a9f2504a8dd27083f89d21894ceb01cc59db" + "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0a62a9f2504a8dd27083f89d21894ceb01cc59db", - "reference": "0a62a9f2504a8dd27083f89d21894ceb01cc59db", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/2888fcdc4dc2fd5f7c7397be78631e8af12e02b4", + "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -1753,7 +1841,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.4.13" + "source": "https://github.com/symfony/options-resolver/tree/v7.4.8" }, "funding": [ { @@ -1764,25 +1852,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:18:03+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -1797,8 +1889,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1832,7 +1924,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -1843,25 +1935,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", "shasum": "" }, "require": { @@ -1873,8 +1969,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1910,7 +2006,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" }, "funding": [ { @@ -1921,25 +2017,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-26T05:58:03+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { @@ -1951,8 +2051,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1991,7 +2091,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -2002,28 +2102,33 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-25T13:48:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/14c5439eec4ccff081ac14eca2dc57feb2a66d92", + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -2035,8 +2140,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -2071,7 +2176,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.1" }, "funding": [ { @@ -2082,25 +2187,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", "shasum": "" }, "require": { @@ -2151,7 +2260,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" }, "funding": [ { @@ -2171,20 +2280,20 @@ "type": "tidelift" } ], - "time": "2025-01-02T08:10:11+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.31.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", "shasum": "" }, "require": { @@ -2193,8 +2302,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -2231,7 +2340,87 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T12:45:58+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" }, "funding": [ { @@ -2242,29 +2431,33 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { "name": "symfony/process", - "version": "v6.4.33", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "c46e854e79b52d07666e43924a20cb6dc546644e" + "reference": "f5804be144caceb570f6747519999636b664f24c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/c46e854e79b52d07666e43924a20cb6dc546644e", - "reference": "c46e854e79b52d07666e43924a20cb6dc546644e", + "url": "https://api.github.com/repos/symfony/process/zipball/f5804be144caceb570f6747519999636b664f24c", + "reference": "f5804be144caceb570f6747519999636b664f24c", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "type": "library", "autoload": { @@ -2292,7 +2485,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.33" + "source": "https://github.com/symfony/process/tree/v7.4.13" }, "funding": [ { @@ -2312,20 +2505,20 @@ "type": "tidelift" } ], - "time": "2026-01-23T16:02:12+00:00" + "time": "2026-05-23T16:05:06+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -2338,12 +2531,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" } }, "autoload": { @@ -2379,7 +2572,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -2390,29 +2583,33 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "symfony/stopwatch", - "version": "v6.4.13", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "2cae0a6f8d04937d02f6d19806251e2104d54f92" + "reference": "70a852d72fec4d51efb1f48dcd968efcaf5ccb89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/2cae0a6f8d04937d02f6d19806251e2104d54f92", - "reference": "2cae0a6f8d04937d02f6d19806251e2104d54f92", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/70a852d72fec4d51efb1f48dcd968efcaf5ccb89", + "reference": "70a852d72fec4d51efb1f48dcd968efcaf5ccb89", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/service-contracts": "^2.5|^3" }, "type": "library", @@ -2441,7 +2638,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v6.4.13" + "source": "https://github.com/symfony/stopwatch/tree/v7.4.8" }, "funding": [ { @@ -2452,31 +2649,36 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:18:03+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/string", - "version": "v6.4.15", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f" + "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", - "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", + "url": "https://api.github.com/repos/symfony/string/zipball/961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", + "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-grapheme": "~1.33", "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0" }, @@ -2484,11 +2686,11 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^5.4|^6.0|^7.0", - "symfony/http-client": "^5.4|^6.0|^7.0", - "symfony/intl": "^6.2|^7.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^5.4|^6.0|^7.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2527,7 +2729,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.15" + "source": "https://github.com/symfony/string/tree/v7.4.13" }, "funding": [ { @@ -2538,12 +2740,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-13T13:31:12+00:00" + "time": "2026-05-23T15:23:29+00:00" } ], "packages-dev": [], @@ -2555,7 +2761,7 @@ "platform": {}, "platform-dev": {}, "platform-overrides": { - "php": "8.1" + "php": "8.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/tools/phpstan/composer.json b/tools/phpstan/composer.json index 309fa9f..0041404 100644 --- a/tools/phpstan/composer.json +++ b/tools/phpstan/composer.json @@ -2,8 +2,9 @@ "type": "project", "require": { "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^1.12.11", - "phpstan/phpstan-symfony": "^1.4.12" + "phpstan/phpstan": "^2.2.1", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-symfony": "^2.0.18" }, "config": { "allow-plugins": { @@ -11,7 +12,7 @@ }, "bump-after-update": true, "platform": { - "php": "8.1" + "php": "8.3" }, "sort-packages": true } diff --git a/tools/phpstan/composer.lock b/tools/phpstan/composer.lock index daa2da1..72ab589 100644 --- a/tools/phpstan/composer.lock +++ b/tools/phpstan/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b4c9d53f182a2429b4b8644af398a8fd", + "content-hash": "5a3da7ccc578c6fe6dcd996c2cf5a40a", "packages": [ { "name": "phpstan/extension-installer", @@ -56,20 +56,15 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.11", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733" - }, + "version": "2.2.1", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", - "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dea9c8f2d25cc849391042b71e429c1a4bf82660", + "reference": "dea9c8f2d25cc849391042b71e429c1a4bf82660", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -88,6 +83,17 @@ "license": [ "MIT" ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], "description": "PHPStan - PHP Static Analysis Tool", "keywords": [ "dev", @@ -110,37 +116,86 @@ "type": "github" } ], - "time": "2024-11-17T14:08:01+00:00" + "time": "2026-05-28T14:44:12+00:00" + }, + { + "name": "phpstan/phpstan-deprecation-rules", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", + "reference": "6b5571001a7f04fa0422254c30a0017ec2f2cacc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/6b5571001a7f04fa0422254c30a0017ec2f2cacc", + "reference": "6b5571001a7f04fa0422254c30a0017ec2f2cacc", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.39" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.4" + }, + "time": "2026-02-09T13:21:14+00:00" }, { "name": "phpstan/phpstan-symfony", - "version": "1.4.12", + "version": "2.0.18", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "c7b7e7f520893621558bfbfdb2694d4364565c1d" + "reference": "a12176b639dec54e8bfd0a5ebf5fc36ffe003b5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/c7b7e7f520893621558bfbfdb2694d4364565c1d", - "reference": "c7b7e7f520893621558bfbfdb2694d4364565c1d", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/a12176b639dec54e8bfd0a5ebf5fc36ffe003b5d", + "reference": "a12176b639dec54e8bfd0a5ebf5fc36ffe003b5d", "shasum": "" }, "require": { "ext-simplexml": "*", - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.13" }, "conflict": { "symfony/framework-bundle": "<3.0" }, "require-dev": { - "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.3.11", - "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^8.5.29 || ^9.5", - "psr/container": "1.0 || 1.1.1", + "phpstan/phpstan-phpunit": "^2.0.8", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "psr/container": "1.1.2", "symfony/config": "^5.4 || ^6.1", "symfony/console": "^5.4 || ^6.1", "symfony/dependency-injection": "^5.4 || ^6.1", @@ -178,11 +233,14 @@ } ], "description": "Symfony Framework extensions and rules for PHPStan", + "keywords": [ + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/phpstan-symfony/issues", - "source": "https://github.com/phpstan/phpstan-symfony/tree/1.4.12" + "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.18" }, - "time": "2024-11-06T10:13:18+00:00" + "time": "2026-05-18T14:51:49+00:00" } ], "packages-dev": [], @@ -194,7 +252,7 @@ "platform": {}, "platform-dev": {}, "platform-overrides": { - "php": "8.1" + "php": "8.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/tools/twig-cs-fixer/.gitignore b/tools/twig-cs-fixer/.gitignore new file mode 100644 index 0000000..57872d0 --- /dev/null +++ b/tools/twig-cs-fixer/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/tools/twig-cs-fixer/composer.json b/tools/twig-cs-fixer/composer.json new file mode 100644 index 0000000..d57a646 --- /dev/null +++ b/tools/twig-cs-fixer/composer.json @@ -0,0 +1,13 @@ +{ + "type": "project", + "require": { + "vincentlanglet/twig-cs-fixer": "^3.14.0" + }, + "config": { + "platform": { + "php": "8.3" + }, + "bump-after-update": true, + "sort-packages": true + } +} diff --git a/tools/twig-cs-fixer/composer.lock b/tools/twig-cs-fixer/composer.lock new file mode 100644 index 0000000..55b5905 --- /dev/null +++ b/tools/twig-cs-fixer/composer.lock @@ -0,0 +1,1118 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "18ccbc88c9cd027c51ba2bf2da5e667a", + "packages": [ + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-24T08:56:14+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-13T15:52:40+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.4.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.4.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-11T16:38:44+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "e0be088d22278583a82da281886e8c3592fbf149" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", + "reference": "e0be088d22278583a82da281886e8c3592fbf149", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T05:58:03+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.38.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:48:31+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/14c5439eec4ccff081ac14eca2dc57feb2a66d92", + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T12:51:13+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-28T09:44:51+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", + "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-23T15:23:29+00:00" + }, + { + "name": "twig/twig", + "version": "v3.27.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "04ae1bfe9463c816cf72ca0abe7eae2c77a9a9ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/04ae1bfe9463c816cf72ca0abe7eae2c77a9a9ed", + "reference": "04ae1bfe9463c816cf72ca0abe7eae2c77a9a9ed", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "php-cs-fixer/shim": "^3.0@stable", + "phpstan/phpstan": "^2.0@stable", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.27.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2026-05-27T13:05:51+00:00" + }, + { + "name": "vincentlanglet/twig-cs-fixer", + "version": "3.14.0", + "source": { + "type": "git", + "url": "https://github.com/VincentLanglet/Twig-CS-Fixer.git", + "reference": "599f110f192c31af5deb5736d6c1a970afdf51f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/VincentLanglet/Twig-CS-Fixer/zipball/599f110f192c31af5deb5736d6c1a970afdf51f3", + "reference": "599f110f192c31af5deb5736d6c1a970afdf51f3", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "ext-ctype": "*", + "php": ">=8.1", + "symfony/console": "^5.4.9 || ^6.4 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/finder": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/string": "^5.4.42 || ^6.4.10 || ~7.0.10 || ^7.1.3 || ^8.0", + "twig/twig": "^3.4", + "webmozart/assert": "^1.10 || ^2.0" + }, + "require-dev": { + "composer/semver": "^3.2.0", + "dereuromark/composer-prefer-lowest": "^0.1.10", + "ergebnis/composer-normalize": "^2.29", + "friendsofphp/php-cs-fixer": "^3.13.0", + "infection/infection": "^0.26.16 || ^0.32.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpstan/phpstan-symfony": "^2.0", + "phpstan/phpstan-webmozart-assert": "^2.0", + "phpunit/phpunit": "^9.5.26 || ^11.5.18 || ^12.1.3", + "rector/rector": "^2.0.0", + "shipmonk/composer-dependency-analyser": "^1.6", + "symfony/process": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/twig-bridge": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/ux-twig-component": "^2.2.0", + "twig/cache-extra": "^3.2" + }, + "bin": [ + "bin/twig-cs-fixer" + ], + "type": "coding-standard", + "autoload": { + "psr-4": { + "TwigCsFixer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Vincent Langlet" + } + ], + "description": "A tool to automatically fix Twig code style", + "homepage": "https://github.com/VincentLanglet/Twig-CS-Fixer", + "support": { + "issues": "https://github.com/VincentLanglet/Twig-CS-Fixer/issues", + "source": "https://github.com/VincentLanglet/Twig-CS-Fixer/tree/3.14.0" + }, + "funding": [ + { + "url": "https://github.com/VincentLanglet", + "type": "github" + } + ], + "time": "2026-02-23T13:21:35+00:00" + }, + { + "name": "webmozart/assert", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9007ea6f45ecf352a9422b36644e4bfc039b9155", + "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "psalm": { + "pluginClass": "Webmozart\\Assert\\PsalmPlugin" + }, + "branch-alias": { + "dev-master": "2.0-dev", + "dev-feature/2-0": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/2.4.0" + }, + "time": "2026-05-20T13:07:01+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "platform-overrides": { + "php": "8.3" + }, + "plugin-api-version": "2.9.0" +}