├── translations └── .gitignore ├── tools ├── php-cs-fixer │ ├── .gitignore │ ├── composer.json │ └── composer.lock ├── bin │ ├── phpstan │ └── php-cs-fixer └── phpstan │ ├── .gitignore │ ├── composer.json │ └── composer.lock ├── infrastructure └── docker │ ├── services │ ├── router │ │ ├── certs │ │ │ └── .gitkeep │ │ ├── Dockerfile │ │ ├── traefik │ │ │ ├── dynamic_conf.yaml │ │ │ └── traefik.yaml │ │ ├── generate-ssl.sh │ │ └── openssl.cnf │ ├── php │ │ ├── frontend │ │ │ ├── etc │ │ │ │ ├── nginx │ │ │ │ │ ├── environments │ │ │ │ │ └── nginx.conf │ │ │ │ └── service │ │ │ │ │ ├── nginx │ │ │ │ │ └── run │ │ │ │ │ └── php-fpm │ │ │ │ │ └── run │ │ │ └── php-configuration │ │ │ │ ├── mods-available │ │ │ │ └── app-fpm.ini │ │ │ │ └── fpm │ │ │ │ └── php-fpm.conf │ │ ├── builder │ │ │ ├── etc │ │ │ │ └── sudoers.d │ │ │ │ │ └── sudo │ │ │ └── php-configuration │ │ │ │ └── mods-available │ │ │ │ └── app-builder.ini │ │ ├── entrypoint │ │ ├── base │ │ │ └── php-configuration │ │ │ │ └── mods-available │ │ │ │ └── app-default.ini │ │ └── Dockerfile │ ├── rabbitmq │ │ ├── Dockerfile │ │ └── etc │ │ │ └── rabbitmq │ │ │ └── rabbitmq.conf │ └── postgres │ │ └── Dockerfile │ ├── docker-compose.worker.yml │ ├── docker-compose.builder.yml │ └── docker-compose.yml ├── .gitattributes ├── config ├── routes │ ├── easyadmin.yaml │ ├── security.yaml │ └── framework.yaml ├── packages │ ├── twig.yaml │ ├── translation.yaml │ ├── doctrine_migrations.yaml │ ├── routing.yaml │ ├── validator.yaml │ ├── framework.yaml │ ├── messenger.yaml │ ├── cache.yaml │ ├── security.yaml │ ├── doctrine.yaml │ └── monolog.yaml ├── routes.yaml ├── preload.php ├── bundles.php └── services.yaml ├── .env.test ├── public └── index.php ├── src ├── Kernel.php ├── MessageHandler │ └── ChangelogHandler.php ├── Entity │ └── Article.php ├── Message │ └── Changelog.php ├── Controller │ └── Admin │ │ ├── ArticleCrudController.php │ │ └── DashboardController.php ├── Repository │ └── ArticleRepository.php └── Messenger │ └── Serializer │ └── ChangelogSerializer.php ├── tests └── bootstrap.php ├── .gitignore ├── templates └── base.html.twig ├── migrations ├── Version20241105180713.php └── Version20241105180714.php ├── bin ├── console └── phpunit ├── .php-cs-fixer.php ├── .castor ├── database.php ├── qa.php └── docker.php ├── phpstan.neon ├── LICENSE ├── phpunit.xml.dist ├── .env ├── castor.php ├── composer.json ├── README.md └── symfony.lock /translations/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/php-cs-fixer/.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | -------------------------------------------------------------------------------- /tools/bin/phpstan: -------------------------------------------------------------------------------- 1 | ../phpstan/vendor/bin/phpstan -------------------------------------------------------------------------------- /tools/phpstan/.gitignore: -------------------------------------------------------------------------------- 1 | /var/ 2 | /vendor/ 3 | -------------------------------------------------------------------------------- /infrastructure/docker/services/router/certs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/bin/php-cs-fixer: -------------------------------------------------------------------------------- 1 | ../php-cs-fixer/vendor/bin/php-cs-fixer -------------------------------------------------------------------------------- /infrastructure/docker/services/php/frontend/etc/nginx/environments: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Force LF line ending (mandatory for Windows) 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /config/routes/easyadmin.yaml: -------------------------------------------------------------------------------- 1 | easyadmin: 2 | resource: . 3 | type: easyadmin.routes 4 | -------------------------------------------------------------------------------- /infrastructure/docker/services/php/builder/etc/sudoers.d/sudo: -------------------------------------------------------------------------------- 1 | %sudo ALL=(ALL) NOPASSWD: ALL 2 | -------------------------------------------------------------------------------- /infrastructure/docker/services/php/frontend/etc/service/nginx/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec /usr/sbin/nginx 4 | -------------------------------------------------------------------------------- /config/routes/security.yaml: -------------------------------------------------------------------------------- 1 | _security_logout: 2 | resource: security.route_loader.logout 3 | type: service 4 | -------------------------------------------------------------------------------- /infrastructure/docker/services/rabbitmq/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rabbitmq:3-management-alpine 2 | 3 | COPY etc/. /etc/ 4 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | file_name_pattern: '*.twig' 3 | 4 | when@test: 5 | twig: 6 | strict_variables: true 7 | -------------------------------------------------------------------------------- /config/routes.yaml: -------------------------------------------------------------------------------- 1 | controllers: 2 | resource: 3 | path: ../src/Controller/ 4 | namespace: App\Controller 5 | type: attribute 6 | -------------------------------------------------------------------------------- /infrastructure/docker/services/rabbitmq/etc/rabbitmq/rabbitmq.conf: -------------------------------------------------------------------------------- 1 | # services/rabbitmq/etc/rabbitmq/rabbitmq.conf 2 | vm_memory_high_watermark.absolute = 1GB 3 | -------------------------------------------------------------------------------- /config/routes/framework.yaml: -------------------------------------------------------------------------------- 1 | when@dev: 2 | _errors: 3 | resource: '@FrameworkBundle/Resources/config/routing/errors.xml' 4 | prefix: /_error 5 | -------------------------------------------------------------------------------- /infrastructure/docker/services/php/frontend/etc/service/php-fpm/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec /usr/sbin/php-fpm${PHP_VERSION} -y /etc/php/${PHP_VERSION}/fpm/php-fpm.conf -O 4 | -------------------------------------------------------------------------------- /infrastructure/docker/services/php/frontend/php-configuration/mods-available/app-fpm.ini: -------------------------------------------------------------------------------- 1 | ; priority=40 2 | [PHP] 3 | expose_php = off 4 | memory_limit = 128M 5 | max_execution_time = 30 6 | -------------------------------------------------------------------------------- /config/packages/translation.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | default_locale: en 3 | translator: 4 | default_path: '%kernel.project_dir%/translations' 5 | fallbacks: 6 | - en 7 | providers: 8 | -------------------------------------------------------------------------------- /infrastructure/docker/services/php/builder/php-configuration/mods-available/app-builder.ini: -------------------------------------------------------------------------------- 1 | ; priority=40 2 | [PHP] 3 | error_log = /var/log/php/error.log 4 | [opcache] 5 | opcache.error_log = /var/log/php/opcache.log 6 | -------------------------------------------------------------------------------- /config/preload.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__) . '/.env'); 9 | } 10 | 11 | if ($_SERVER['APP_DEBUG']) { 12 | umask(0o000); 13 | } 14 | -------------------------------------------------------------------------------- /tools/php-cs-fixer/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "project", 3 | "require": { 4 | "friendsofphp/php-cs-fixer": "^3.64.0" 5 | }, 6 | "config": { 7 | "platform": { 8 | "php": "8.1" 9 | }, 10 | "bump-after-update": true, 11 | "sort-packages": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /infrastructure/docker/services/router/traefik/dynamic_conf.yaml: -------------------------------------------------------------------------------- 1 | tls: 2 | stores: 3 | default: 4 | defaultCertificate: 5 | certFile: /etc/ssl/certs/cert.pem 6 | keyFile: /etc/ssl/certs/key.pem 7 | 8 | http: 9 | middlewares: 10 | redirect-to-https: 11 | redirectScheme: 12 | scheme: https 13 | -------------------------------------------------------------------------------- /src/MessageHandler/ChangelogHandler.php: -------------------------------------------------------------------------------- 1 | id = uuid_create(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tools/phpstan/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "project", 3 | "require": { 4 | "phpstan/extension-installer": "^1.4.3", 5 | "phpstan/phpstan": "^1.12.7", 6 | "phpstan/phpstan-symfony": "^1.4.11" 7 | }, 8 | "config": { 9 | "allow-plugins": { 10 | "phpstan/extension-installer": true 11 | }, 12 | "bump-after-update": true, 13 | "platform": { 14 | "php": "8.1" 15 | }, 16 | "sort-packages": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /infrastructure/docker/services/php/frontend/php-configuration/fpm/php-fpm.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | pid = /var/run/php-fpm.pid 3 | error_log = /proc/self/fd/2 4 | daemonize = no 5 | 6 | [www] 7 | user = app 8 | group = app 9 | listen = 127.0.0.1:9000 10 | pm = dynamic 11 | pm.max_children = 25 12 | pm.start_servers = 2 13 | pm.min_spare_servers = 2 14 | pm.max_spare_servers = 3 15 | pm.max_requests = 500 16 | pm.status_path = /php-fpm-status 17 | clear_env = no 18 | request_terminate_timeout = 120s 19 | catch_workers_output = yes 20 | -------------------------------------------------------------------------------- /infrastructure/docker/docker-compose.worker.yml: -------------------------------------------------------------------------------- 1 | # this is a template to factorize the service definitions 2 | x-services-templates: 3 | worker_base: &worker_base 4 | build: 5 | context: services/php 6 | target: worker 7 | # Don't use depends_on, it does not work well with docker compose profiles 8 | volumes: 9 | - "../..:/var/www:cached" 10 | profiles: 11 | - worker 12 | 13 | # services: 14 | # worker_messenger: 15 | # <<: *worker_base 16 | # command: php -d memory_limit=1G /var/www/bin/console messenger:consume async --memory-limit=128M 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ###> symfony/framework-bundle ### 3 | /.env.local 4 | /.env.local.php 5 | /.env.*.local 6 | /config/secrets/prod/prod.decrypt.private.php 7 | /public/bundles/ 8 | /var/ 9 | /vendor/ 10 | ###< symfony/framework-bundle ### 11 | /.castor.stub.php 12 | /infrastructure/docker/docker-compose.override.yml 13 | /infrastructure/docker/services/router/certs/*.pem 14 | 15 | # Tools 16 | .php-cs-fixer.cache 17 | 18 | ###> symfony/phpunit-bridge ### 19 | .phpunit.result.cache 20 | /phpunit.xml 21 | ###< symfony/phpunit-bridge ### 22 | 23 | ###> phpunit/phpunit ### 24 | /phpunit.xml 25 | .phpunit.result.cache 26 | ###< phpunit/phpunit ### 27 | -------------------------------------------------------------------------------- /src/Message/Changelog.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Welcome!{% endblock %} 6 | 7 | {% block stylesheets %} 8 | {% endblock %} 9 | 10 | {% block javascripts %} 11 | {% endblock %} 12 | 13 | 14 | {% block body %}{% endblock %} 15 | 16 | 17 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 6 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 7 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 8 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 9 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 10 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 11 | EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true], 12 | ]; 13 | -------------------------------------------------------------------------------- /migrations/Version20241105180713.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE TABLE article (id uuid NOT NULL, title VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); 18 | } 19 | 20 | public function down(Schema $schema): void 21 | { 22 | $this->addSql('DROP TABLE article'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /infrastructure/docker/services/php/entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | set -u 5 | 6 | if [ $(id -u) != 0 ]; then 7 | echo "Running this image as non root is not allowed" 8 | exit 1 9 | fi 10 | 11 | : "${UID:=0}" 12 | : "${GID:=${UID}}" 13 | 14 | if [ "$#" = 0 ]; then 15 | set -- "$(command -v bash 2>/dev/null || command -v sh)" -l 16 | fi 17 | 18 | if [ "$UID" != 0 ]; then 19 | usermod -u "$UID" "{{ application_user }}" >/dev/null 2>/dev/null && { 20 | groupmod -g "$GID" "{{ application_user }}" >/dev/null 2>/dev/null || 21 | usermod -a -G "$GID" "{{ application_user }}" >/dev/null 2>/dev/null 22 | } 23 | set -- gosu "${UID}:${GID}" "${@}" 24 | fi 25 | 26 | exec "$@" 27 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | = 80000) { 10 | require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit'; 11 | } else { 12 | define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php'); 13 | require PHPUNIT_COMPOSER_INSTALL; 14 | PHPUnit\TextUI\Command::main(); 15 | } 16 | } else { 17 | if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) { 18 | echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n"; 19 | exit(1); 20 | } 21 | 22 | require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php'; 23 | } 24 | -------------------------------------------------------------------------------- /src/Controller/Admin/ArticleCrudController.php: -------------------------------------------------------------------------------- 1 | ignoreVCSIgnored(true) 5 | ->ignoreDotFiles(false) 6 | ->in(__DIR__) 7 | ->append([ 8 | __FILE__, 9 | ]) 10 | ; 11 | 12 | return (new PhpCsFixer\Config()) 13 | ->setRiskyAllowed(true) 14 | ->setRules([ 15 | '@PHP83Migration' => true, 16 | '@PhpCsFixer' => true, 17 | '@Symfony' => true, 18 | '@Symfony:risky' => true, 19 | 'php_unit_internal_class' => false, // From @PhpCsFixer but we don't want it 20 | 'php_unit_test_class_requires_covers' => false, // From @PhpCsFixer but we don't want it 21 | 'phpdoc_add_missing_param_annotation' => false, // From @PhpCsFixer but we don't want it 22 | 'concat_space' => ['spacing' => 'one'], 23 | 'ordered_class_elements' => true, // Symfony(PSR12) override the default value, but we don't want 24 | 'blank_line_before_statement' => true, // Symfony(PSR12) override the default value, but we don't want 25 | ]) 26 | ->setFinder($finder) 27 | ; 28 | -------------------------------------------------------------------------------- /src/Controller/Admin/DashboardController.php: -------------------------------------------------------------------------------- 1 | redirectToRoute('admin_article_index'); 18 | } 19 | 20 | public function configureDashboard(): Dashboard 21 | { 22 | return Dashboard::new() 23 | ->setTitle('DB to rabbitmq to messenger'); 24 | } 25 | 26 | public function configureMenuItems(): iterable 27 | { 28 | // yield MenuItem::linkToDashboard('Dashboard', 'fa fa-home'); 29 | yield MenuItem::linkToCrud('Article', 'fas fa-list', Article::class); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.castor/database.php: -------------------------------------------------------------------------------- 1 | title('Connecting to the PostgreSQL database'); 16 | 17 | docker_compose(['exec', 'postgres', 'psql', '-U', 'app', 'app'], context()->toInteractive()); 18 | } 19 | 20 | #[AsTask(description: 'Reset database', name: 'reset')] 21 | function reset(bool $test = false): void 22 | { 23 | io()->title('Resetting the database'); 24 | 25 | $suffix = ''; 26 | if ($test) { 27 | $suffix = '--env=test'; 28 | } 29 | 30 | docker_compose_run("bin/console doctrine:database:drop --force --if-exists {$suffix}"); 31 | docker_compose_run("bin/console doctrine:database:create {$suffix}"); 32 | docker_compose_run("bin/console doctrine:migrations:migrate --no-interaction {$suffix}"); 33 | } 34 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 6 3 | paths: 4 | - src 5 | - public 6 | - castor.php 7 | - .castor/ 8 | scanFiles: 9 | - .castor.stub.php 10 | scanDirectories: 11 | - vendor 12 | tmpDir: tools/phpstan/var 13 | inferPrivatePropertyTypeFromConstructor: true 14 | 15 | # symfony: 16 | # container_xml_path: 'var/cache/dev/App_KernelDevDebugContainer.xml' 17 | 18 | typeAliases: 19 | ContextData: ''' 20 | array{ 21 | project_name: string, 22 | root_domain: string, 23 | extra_domains: string[], 24 | php_version: string, 25 | docker_compose_files: string[], 26 | macos: bool, 27 | power_shell: bool, 28 | user_id: int, 29 | root_dir: string, 30 | env: string, 31 | composer_cache_dir: string, 32 | } 33 | ''' 34 | 35 | ignoreErrors: 36 | # - identifier: missingType.generics 37 | - identifier: missingType.iterableValue 38 | 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Grégoire Pineau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/services.yaml: -------------------------------------------------------------------------------- 1 | # This file is the entry point to configure your own services. 2 | # Files in the packages/ subdirectory configure your dependencies. 3 | 4 | # Put parameters here that don't need to change on each machine where the app is deployed 5 | # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration 6 | parameters: 7 | 8 | services: 9 | # default configuration for services in *this* file 10 | _defaults: 11 | autowire: true # Automatically injects dependencies in your services. 12 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. 13 | 14 | # makes classes in src/ available to be used as services 15 | # this creates a service per class whose id is the fully-qualified class name 16 | App\: 17 | resource: '../src/' 18 | exclude: 19 | - '../src/DependencyInjection/' 20 | - '../src/Entity/' 21 | - '../src/Kernel.php' 22 | 23 | # add more service definitions when explicit configuration is needed 24 | # please note that last definitions always *replace* previous ones 25 | -------------------------------------------------------------------------------- /infrastructure/docker/services/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:16 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y --no-install-recommends \ 5 | build-essential \ 6 | ca-certificates \ 7 | postgresql-server-dev-16 \ 8 | python3 \ 9 | python3-dev \ 10 | python3-pip \ 11 | python3-setuptools \ 12 | unzip \ 13 | wget \ 14 | && apt-get clean \ 15 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* 16 | 17 | # https://github.com/pgsql-io/multicorn2 18 | RUN wget https://github.com/pgsql-io/multicorn2/archive/refs/tags/v3.0.tar.gz -O /tmp/multicorn2.tar.gz \ 19 | && tar -xzf /tmp/multicorn2.tar.gz -C /tmp \ 20 | && cd /tmp/multicorn2-3.0 \ 21 | && make \ 22 | && make install \ 23 | && rm -rf /tmp/multicorn2.tar.gz /tmp/multicorn2-3.0 24 | 25 | # https://github.com/OleksandrBesan/rabbitmq_fdw?tab=readme-ov-file 26 | RUN wget https://github.com/AlexandrBesan/rabbitmq_fdw/archive/refs/heads/main.zip -O /tmp/rabbitmq_fdw.zip \ 27 | && unzip /tmp/rabbitmq_fdw.zip -d /tmp \ 28 | && cd /tmp/rabbitmq_fdw-main \ 29 | && python3 setup.py install \ 30 | && rm -rf /tmp/rabbitmq_fdw.zip /tmp/rabbitmq_fdw-main 31 | 32 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | tests 23 | 24 | 25 | 26 | 27 | 28 | src 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/Repository/ArticleRepository.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class ArticleRepository extends ServiceEntityRepository 13 | { 14 | public function __construct(ManagerRegistry $registry) 15 | { 16 | parent::__construct($registry, Article::class); 17 | } 18 | 19 | // /** 20 | // * @return Article[] Returns an array of Article objects 21 | // */ 22 | // public function findByExampleField($value): array 23 | // { 24 | // return $this->createQueryBuilder('a') 25 | // ->andWhere('a.exampleField = :val') 26 | // ->setParameter('val', $value) 27 | // ->orderBy('a.id', 'ASC') 28 | // ->setMaxResults(10) 29 | // ->getQuery() 30 | // ->getResult() 31 | // ; 32 | // } 33 | 34 | // public function findOneBySomeField($value): ?Article 35 | // { 36 | // return $this->createQueryBuilder('a') 37 | // ->andWhere('a.exampleField = :val') 38 | // ->setParameter('val', $value) 39 | // ->getQuery() 40 | // ->getOneOrNullResult() 41 | // ; 42 | // } 43 | } 44 | -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords 3 | password_hashers: 4 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' 5 | # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider 6 | providers: 7 | users_in_memory: { memory: null } 8 | firewalls: 9 | dev: 10 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 11 | security: false 12 | main: 13 | lazy: true 14 | provider: users_in_memory 15 | 16 | # activate different ways to authenticate 17 | # https://symfony.com/doc/current/security.html#the-firewall 18 | 19 | # https://symfony.com/doc/current/security/impersonating_user.html 20 | # switch_user: true 21 | 22 | # Easy way to control access for large sections of your site 23 | # Note: Only the *first* access control that matches will be used 24 | access_control: 25 | # - { path: ^/admin, roles: ROLE_ADMIN } 26 | # - { path: ^/profile, roles: ROLE_USER } 27 | 28 | when@test: 29 | security: 30 | password_hashers: 31 | # By default, password hashers are resource intensive and take time. This is 32 | # important to generate secure password hashes. In tests however, secure hashes 33 | # are not important, waste resources and increase test times. The following 34 | # reduces the work factor to the lowest possible values. 35 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 36 | algorithm: auto 37 | cost: 4 # Lowest possible value for bcrypt 38 | time_cost: 3 # Lowest possible value for argon 39 | memory_cost: 10 # Lowest possible value for argon 40 | -------------------------------------------------------------------------------- /src/Messenger/Serializer/ChangelogSerializer.php: -------------------------------------------------------------------------------- 1 | serializer->deserialize($encodedEnvelope['body'], Changelog::class, 'json'); 39 | } catch (ExceptionInterface $e) { 40 | throw new MessageDecodingFailedException($e->getMessage(), $e->getCode(), $e); 41 | } 42 | 43 | return new Envelope($changelog, $stamps); 44 | } 45 | 46 | public function encode(Envelope $envelope): array 47 | { 48 | return [ 49 | 'body' => $this->serializer->serialize($envelope->getMessage(), 'json'), 50 | 'headers' => ['type' => 'application/json'], 51 | ]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | url: '%env(resolve:DATABASE_URL)%' 4 | 5 | # IMPORTANT: You MUST configure your server version, 6 | # either here or in the DATABASE_URL env var (see .env file) 7 | #server_version: '16' 8 | 9 | profiling_collect_backtrace: '%kernel.debug%' 10 | use_savepoints: true 11 | orm: 12 | auto_generate_proxy_classes: true 13 | enable_lazy_ghost_objects: true 14 | report_fields_where_declared: true 15 | validate_xml_mapping: true 16 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware 17 | identity_generation_preferences: 18 | Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity 19 | auto_mapping: true 20 | mappings: 21 | App: 22 | type: attribute 23 | is_bundle: false 24 | dir: '%kernel.project_dir%/src/Entity' 25 | prefix: 'App\Entity' 26 | alias: App 27 | controller_resolver: 28 | auto_mapping: false 29 | 30 | when@test: 31 | doctrine: 32 | dbal: 33 | # "TEST_TOKEN" is typically set by ParaTest 34 | dbname_suffix: '_test%env(default::TEST_TOKEN)%' 35 | 36 | when@prod: 37 | doctrine: 38 | orm: 39 | auto_generate_proxy_classes: false 40 | proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' 41 | query_cache_driver: 42 | type: pool 43 | pool: doctrine.system_cache_pool 44 | result_cache_driver: 45 | type: pool 46 | pool: doctrine.result_cache_pool 47 | 48 | framework: 49 | cache: 50 | pools: 51 | doctrine.result_cache_pool: 52 | adapter: cache.app 53 | doctrine.system_cache_pool: 54 | adapter: cache.system 55 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # In all environments, the following files are loaded if they exist, 2 | # the latter taking precedence over the former: 3 | # 4 | # * .env contains default values for the environment variables needed by the app 5 | # * .env.local uncommitted file with local overrides 6 | # * .env.$APP_ENV committed environment-specific defaults 7 | # * .env.$APP_ENV.local uncommitted environment-specific overrides 8 | # 9 | # Real environment variables win over .env files. 10 | # 11 | # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. 12 | # https://symfony.com/doc/current/configuration/secrets.html 13 | # 14 | # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). 15 | # https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration 16 | 17 | ###> symfony/framework-bundle ### 18 | APP_ENV=dev 19 | APP_SECRET=9702261f6298d0ad440dd2b292d315cd 20 | ###< symfony/framework-bundle ### 21 | 22 | ###> doctrine/doctrine-bundle ### 23 | # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url 24 | # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml 25 | # 26 | # DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" 27 | # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4" 28 | # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" 29 | DATABASE_URL="postgresql://app:app@postgres:5432/app?serverVersion=16&charset=utf8" 30 | ###< doctrine/doctrine-bundle ### 31 | 32 | ###> symfony/messenger ### 33 | # Choose one of the transports below 34 | MESSENGER_TRANSPORT_DSN=amqp://guest:guest@rabbitmq:5672/%2f/messages 35 | # MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages 36 | # MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 37 | ###< symfony/messenger ### 38 | 39 | SYMFONY_IDE="vscode://file%f:%l&/var/www>/home/gregoire/dev/labs/symfony/db-to-rabbitmq-to-messenger" 40 | -------------------------------------------------------------------------------- /.castor/qa.php: -------------------------------------------------------------------------------- 1 | title('Installing QA tooling'); 27 | 28 | docker_compose_run('composer install -o', workDir: '/var/www/tools/php-cs-fixer'); 29 | docker_compose_run('composer install -o', workDir: '/var/www/tools/phpstan'); 30 | } 31 | 32 | #[AsTask(description: 'Update tooling')] 33 | function update(): void 34 | { 35 | io()->title('Update QA tooling'); 36 | 37 | docker_compose_run('composer update -o', workDir: '/var/www/tools/php-cs-fixer'); 38 | docker_compose_run('composer update -o', workDir: '/var/www/tools/phpstan'); 39 | } 40 | 41 | #[AsTask(description: 'Runs PHPUnit', aliases: ['phpunit'])] 42 | function phpunit(): int 43 | { 44 | return docker_exit_code('vendor/bin/simple-phpunit'); 45 | } 46 | 47 | #[AsTask(description: 'Runs PHPStan', aliases: ['phpstan'])] 48 | function phpstan(): int 49 | { 50 | if (!is_dir(variable('root_dir') . '/tools/phpstan/vendor')) { 51 | io()->error('PHPStan is not installed. Run `castor qa:install` first.'); 52 | 53 | return 1; 54 | } 55 | 56 | return docker_exit_code('phpstan', workDir: '/var/www'); 57 | } 58 | 59 | #[AsTask(description: 'Fixes Coding Style', aliases: ['cs'])] 60 | function cs(bool $dryRun = false): int 61 | { 62 | if (!is_dir(variable('root_dir') . '/tools/php-cs-fixer/vendor')) { 63 | io()->error('PHP-CS-Fixer is not installed. Run `castor qa:install` first.'); 64 | 65 | return 1; 66 | } 67 | 68 | if ($dryRun) { 69 | return docker_exit_code('php-cs-fixer fix --dry-run --diff', workDir: '/var/www'); 70 | } 71 | 72 | return docker_exit_code('php-cs-fixer fix', workDir: '/var/www'); 73 | } 74 | -------------------------------------------------------------------------------- /config/packages/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | channels: 3 | - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists 4 | 5 | when@dev: 6 | monolog: 7 | handlers: 8 | main: 9 | type: stream 10 | path: "%kernel.logs_dir%/%kernel.environment%.log" 11 | level: debug 12 | channels: ["!event"] 13 | # uncomment to get logging in your browser 14 | # you may have to allow bigger header sizes in your Web server configuration 15 | #firephp: 16 | # type: firephp 17 | # level: info 18 | #chromephp: 19 | # type: chromephp 20 | # level: info 21 | console: 22 | type: console 23 | process_psr_3_messages: false 24 | channels: ["!event", "!doctrine", "!console"] 25 | 26 | when@test: 27 | monolog: 28 | handlers: 29 | main: 30 | type: fingers_crossed 31 | action_level: error 32 | handler: nested 33 | excluded_http_codes: [404, 405] 34 | channels: ["!event"] 35 | nested: 36 | type: stream 37 | path: "%kernel.logs_dir%/%kernel.environment%.log" 38 | level: debug 39 | 40 | when@prod: 41 | monolog: 42 | handlers: 43 | main: 44 | type: fingers_crossed 45 | action_level: error 46 | handler: nested 47 | excluded_http_codes: [404, 405] 48 | buffer_size: 50 # How many messages should be saved? Prevent memory leaks 49 | nested: 50 | type: stream 51 | path: php://stderr 52 | level: debug 53 | formatter: monolog.formatter.json 54 | console: 55 | type: console 56 | process_psr_3_messages: false 57 | channels: ["!event", "!doctrine"] 58 | deprecation: 59 | type: stream 60 | channels: [deprecation] 61 | path: php://stderr 62 | formatter: monolog.formatter.json 63 | -------------------------------------------------------------------------------- /infrastructure/docker/services/php/frontend/etc/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | pid /var/run/nginx.pid; 3 | daemon off; 4 | error_log /proc/self/fd/2; 5 | include /etc/nginx/modules-enabled/*.conf; 6 | 7 | http { 8 | access_log /proc/self/fd/1; 9 | sendfile on; 10 | tcp_nopush on; 11 | tcp_nodelay on; 12 | keepalive_timeout 65; 13 | types_hash_max_size 2048; 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | client_max_body_size 20m; 17 | server_tokens off; 18 | 19 | gzip on; 20 | gzip_disable "msie6"; 21 | gzip_vary on; 22 | gzip_proxied any; 23 | gzip_comp_level 6; 24 | gzip_buffers 16 8k; 25 | gzip_http_version 1.1; 26 | gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; 27 | 28 | server { 29 | listen 0.0.0.0:80; 30 | root /var/www/public; 31 | 32 | location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg)$ { 33 | access_log off; 34 | add_header Cache-Control "no-cache"; 35 | } 36 | 37 | # Remove this block if you want to access to PHP FPM monitoring 38 | # dashboard (on URL: /php-fpm-status). WARNING: on production, you must 39 | # secure this page (by user IP address, with a password, for example) 40 | location ~ ^/php-fpm-status$ { 41 | deny all; 42 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 43 | fastcgi_index index.php; 44 | include fastcgi_params; 45 | fastcgi_pass 127.0.0.1:9000; 46 | } 47 | 48 | location / { 49 | # try to serve file directly, fallback to index.php 50 | try_files $uri /index.php$is_args$args; 51 | } 52 | 53 | location ~ ^/index\.php(/|$) { 54 | fastcgi_pass 127.0.0.1:9000; 55 | fastcgi_split_path_info ^(.+\.php)(/.*)$; 56 | 57 | include fastcgi_params; 58 | include environments; 59 | 60 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 61 | fastcgi_param HTTPS on; 62 | fastcgi_param SERVER_NAME $http_host; 63 | # # Uncomment if you want to use /php-fpm-status endpoint **with** 64 | # # real request URI. It may have some side effects, that's why it's 65 | # # commented by default 66 | # fastcgi_param SCRIPT_NAME $request_uri; 67 | } 68 | 69 | error_log /proc/self/fd/2; 70 | access_log /proc/self/fd/1; 71 | } 72 | } 73 | 74 | events {} 75 | -------------------------------------------------------------------------------- /infrastructure/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - docker-compose.worker.yml 3 | 4 | volumes: 5 | postgres-data: {} 6 | rabbitmq-data: {} 7 | 8 | services: 9 | router: 10 | build: services/router 11 | volumes: 12 | - "/var/run/docker.sock:/var/run/docker.sock" 13 | - "./services/router/certs:/etc/ssl/certs" 14 | ports: 15 | - "80:80" 16 | - "443:443" 17 | - "8080:8080" 18 | networks: 19 | - default 20 | profiles: 21 | - default 22 | 23 | frontend: 24 | build: 25 | context: services/php 26 | target: frontend 27 | depends_on: 28 | postgres: 29 | condition: service_healthy 30 | volumes: 31 | - "../..:/var/www:cached" 32 | profiles: 33 | - default 34 | labels: 35 | - "traefik.enable=true" 36 | - "project-name=${PROJECT_NAME}" 37 | - "traefik.http.routers.${PROJECT_NAME}-frontend.rule=Host(${PROJECT_DOMAINS})" 38 | - "traefik.http.routers.${PROJECT_NAME}-frontend.tls=true" 39 | - "traefik.http.routers.${PROJECT_NAME}-frontend-unsecure.rule=Host(${PROJECT_DOMAINS})" 40 | # Comment the next line to be able to access frontend via HTTP instead of HTTPS 41 | - "traefik.http.routers.${PROJECT_NAME}-frontend-unsecure.middlewares=redirect-to-https@file" 42 | 43 | postgres: 44 | build: services/postgres 45 | environment: 46 | - POSTGRES_USER=app 47 | - POSTGRES_PASSWORD=app 48 | volumes: 49 | - postgres-data:/var/lib/postgresql/data 50 | healthcheck: 51 | test: ["CMD-SHELL", "pg_isready -U postgres"] 52 | interval: 5s 53 | timeout: 5s 54 | retries: 5 55 | profiles: 56 | - default 57 | 58 | rabbitmq: 59 | build: services/rabbitmq 60 | volumes: 61 | - rabbitmq-data:/var/lib/rabbitmq 62 | labels: 63 | - "traefik.enable=true" 64 | - "project-name=${PROJECT_NAME}" 65 | - "traefik.http.routers.${PROJECT_NAME}-rabbitmq.rule=Host(`rabbitmq.${PROJECT_ROOT_DOMAIN}`)" 66 | - "traefik.http.routers.${PROJECT_NAME}-rabbitmq.tls=true" 67 | - "traefik.http.services.rabbitmq.loadbalancer.server.port=15672" 68 | healthcheck: 69 | test: "rabbitmqctl eval '{ true, rabbit_app_booted_and_running } = { rabbit:is_booted(node()), rabbit_app_booted_and_running }, { [], no_alarms } = { rabbit:alarms(), no_alarms }, [] /= rabbit_networking:active_listeners(), rabbitmq_node_is_healthy.' || exit 1" 70 | interval: 5s 71 | timeout: 5s 72 | retries: 5 73 | profiles: 74 | - default 75 | -------------------------------------------------------------------------------- /migrations/Version20241105180714.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE EXTENSION multicorn'); 18 | $this->addSql(<<<'SQL' 19 | CREATE SERVER rabbitmq FOREIGN DATA WRAPPER multicorn 20 | OPTIONS ( 21 | wrapper 'rabbitmq_fdw.RabbitmqFDW', 22 | host 'rabbitmq', 23 | virtual_host '/', 24 | port '5672', 25 | exchange '' 26 | ) 27 | SQL); 28 | $this->addSql(<<<'SQL' 29 | CREATE FOREIGN TABLE changelog ( 30 | body text 31 | ) 32 | SERVER rabbitmq 33 | OPTIONS ( 34 | bulk_size '10', 35 | queue 'changelog', 36 | column 'body' 37 | ) 38 | SQL); 39 | $this->addSql(<<<'SQL' 40 | create or replace function changelog_trigger() returns trigger as $$ 41 | declare 42 | action text; 43 | table_name text; 44 | transaction_id bigint; 45 | timestamp timestamp; 46 | old_data jsonb; 47 | new_data jsonb; 48 | begin 49 | action := lower(TG_OP::text); 50 | table_name := TG_TABLE_NAME::text; 51 | transaction_id := txid_current(); 52 | timestamp := current_timestamp; 53 | 54 | if TG_OP = 'DELETE' then 55 | old_data := to_jsonb(OLD.*); 56 | elseif TG_OP = 'INSERT' then 57 | new_data := to_jsonb(NEW.*); 58 | elseif TG_OP = 'UPDATE' then 59 | old_data := to_jsonb(OLD.*); 60 | new_data := to_jsonb(NEW.*); 61 | end if; 62 | 63 | insert into changelog (body) 64 | values (json_build_object('action', action, 'table_name', table_name, 'transaction_id', transaction_id, 'timestamp', timestamp, 'old_data', old_data, 'new_data', new_data)::text); 65 | 66 | return null; 67 | end; 68 | $$ language plpgsql 69 | SQL); 70 | $this->addSql(<<<'SQL' 71 | create trigger article_changelog_trigger 72 | after insert or update or delete on article 73 | for each row execute function changelog_trigger() 74 | SQL); 75 | } 76 | 77 | public function down(Schema $schema): void 78 | { 79 | $this->throwIrreversibleMigrationException(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /castor.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | function create_default_variables(): array 27 | { 28 | return [ 29 | 'project_name' => 'db-to-rabbitmq-to-messenger', 30 | 'root_domain' => 'db-to-rabbitmq-to-messenger.test', 31 | ]; 32 | } 33 | 34 | #[AsTask(description: 'Builds and starts the infrastructure, then install the application (composer, yarn, ...)')] 35 | function start(): void 36 | { 37 | io()->title('Starting the stack'); 38 | 39 | // workers_stop(); 40 | generate_certificates(force: false); 41 | build(); 42 | up(profiles: ['default']); // We can't start worker now, they are not installed 43 | cache_clear(); 44 | install(); 45 | migrate(); 46 | // workers_start(); 47 | 48 | notify('The stack is now up and running.'); 49 | io()->success('The stack is now up and running.'); 50 | 51 | about(); 52 | } 53 | 54 | #[AsTask(description: 'Installs the application (composer, yarn, ...)', namespace: 'app', aliases: ['install'])] 55 | function install(): void 56 | { 57 | io()->title('Installing the application'); 58 | 59 | io()->section('Installing PHP dependencies'); 60 | docker_compose_run('composer install -n --prefer-dist --optimize-autoloader'); 61 | 62 | if (is_file(PathHelper::getRoot() . '/importmap.php')) { 63 | io()->section('Installing importmap'); 64 | docker_compose_run('bin/console importmap:install'); 65 | } 66 | 67 | qa\install(); 68 | } 69 | 70 | #[AsTask(description: 'Clear the application cache', namespace: 'app', aliases: ['cache-clear'])] 71 | function cache_clear(): void 72 | { 73 | io()->title('Clearing the application cache'); 74 | 75 | docker_compose_run('rm -rf var/cache/'); 76 | // On the very first run, the vendor does not exist yet 77 | if (is_dir(PathHelper::getRoot() . '/vendor')) { 78 | docker_compose_run('bin/console cache:warmup'); 79 | } 80 | } 81 | 82 | #[AsTask(description: 'Migrates database schema', namespace: 'app:db', aliases: ['migrate'])] 83 | function migrate(): void 84 | { 85 | io()->title('Migrating the database schema'); 86 | 87 | docker_compose_run('bin/console doctrine:database:create --if-not-exists'); 88 | docker_compose_run('bin/console doctrine:migration:migrate -n --allow-no-migration --all-or-nothing'); 89 | } 90 | 91 | #[AsTask(description: 'Loads fixtures', namespace: 'app:db', aliases: ['fixture'])] 92 | function fixtures(): void 93 | { 94 | io()->title('Loads fixtures'); 95 | 96 | docker_compose_run('bin/console doctrine:fixture:load -n'); 97 | } 98 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lyrixx/db-to-rabbitmq-to-messenger", 3 | "description": "POC to dispatch DB event to rabbitmq, then consume it with messenger", 4 | "type": "project", 5 | "license": "MIT", 6 | "minimum-stability": "stable", 7 | "prefer-stable": true, 8 | "require": { 9 | "php": ">=8.2", 10 | "ext-amqp": "*", 11 | "ext-ctype": "*", 12 | "ext-iconv": "*", 13 | "doctrine/dbal": "^3", 14 | "doctrine/doctrine-bundle": "^2.13", 15 | "doctrine/doctrine-migrations-bundle": "^3.3", 16 | "doctrine/orm": "^3.3", 17 | "easycorp/easyadmin-bundle": "^4.14", 18 | "phpdocumentor/reflection-docblock": "^5.5", 19 | "phpstan/phpdoc-parser": "^1.33", 20 | "symfony/amqp-messenger": "7.1.*", 21 | "symfony/console": "7.1.*", 22 | "symfony/doctrine-messenger": "7.1.*", 23 | "symfony/dotenv": "7.1.*", 24 | "symfony/flex": "^2", 25 | "symfony/form": "7.1.*", 26 | "symfony/framework-bundle": "7.1.*", 27 | "symfony/messenger": "7.1.*", 28 | "symfony/monolog-bundle": "^3.10", 29 | "symfony/property-access": "7.1.*", 30 | "symfony/property-info": "7.1.*", 31 | "symfony/runtime": "7.1.*", 32 | "symfony/security-csrf": "7.1.*", 33 | "symfony/serializer": "7.1.*", 34 | "symfony/twig-bundle": "7.1.*", 35 | "symfony/validator": "7.1.*", 36 | "symfony/yaml": "7.1.*" 37 | }, 38 | "config": { 39 | "allow-plugins": { 40 | "php-http/discovery": true, 41 | "symfony/flex": true, 42 | "symfony/runtime": true 43 | }, 44 | "sort-packages": true 45 | }, 46 | "autoload": { 47 | "psr-4": { 48 | "App\\": "src/" 49 | } 50 | }, 51 | "autoload-dev": { 52 | "psr-4": { 53 | "App\\Tests\\": "tests/" 54 | } 55 | }, 56 | "replace": { 57 | "symfony/polyfill-ctype": "*", 58 | "symfony/polyfill-iconv": "*", 59 | "symfony/polyfill-php72": "*", 60 | "symfony/polyfill-php73": "*", 61 | "symfony/polyfill-php74": "*", 62 | "symfony/polyfill-php80": "*", 63 | "symfony/polyfill-php81": "*", 64 | "symfony/polyfill-php82": "*" 65 | }, 66 | "scripts": { 67 | "auto-scripts": { 68 | "cache:clear": "symfony-cmd", 69 | "assets:install %PUBLIC_DIR%": "symfony-cmd" 70 | }, 71 | "post-install-cmd": [ 72 | "@auto-scripts" 73 | ], 74 | "post-update-cmd": [ 75 | "@auto-scripts" 76 | ] 77 | }, 78 | "conflict": { 79 | "symfony/symfony": "*" 80 | }, 81 | "extra": { 82 | "symfony": { 83 | "allow-contrib": false, 84 | "require": "7.1.*", 85 | "docker": false 86 | } 87 | }, 88 | "require-dev": { 89 | "phpunit/phpunit": "^9.5", 90 | "symfony/browser-kit": "7.1.*", 91 | "symfony/css-selector": "7.1.*", 92 | "symfony/maker-bundle": "^1.61", 93 | "symfony/phpunit-bridge": "^7.1" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DB to RabbitMQ to Messenger 2 | 3 | ## Intro 4 | 5 | This is a **Proof Of Concept**. I wanted to explore something: Is it possible to 6 | automatically record DB changes to RabbitMQ, and then process them with Symfony 7 | Messenger. Spoiler: Yes! It works really well 8 | 9 | ## Running the application locally 10 | 11 | ### Requirements 12 | 13 | A Docker environment is provided and requires you to have these tools available: 14 | 15 | * Docker 16 | * Bash 17 | * [Castor](https://github.com/jolicode/castor#installation) 18 | 19 | #### Castor 20 | 21 | Once castor is installed, in order to improve your usage of castor scripts, you 22 | can install console autocompletion script. 23 | 24 | If you are using bash: 25 | 26 | ```bash 27 | castor completion | sudo tee /etc/bash_completion.d/castor 28 | ``` 29 | 30 | If you are using something else, please refer to your shell documentation. You 31 | may need to use `castor completion > /to/somewhere`. 32 | 33 | Castor supports completion for `bash`, `zsh` & `fish` shells. 34 | 35 | ### Docker environment 36 | 37 | The Docker infrastructure provides a web stack with: 38 | - NGINX 39 | - PostgreSQL 40 | - RabbitMq 41 | - PHP 42 | - Traefik 43 | - A container with some tooling: 44 | - Composer 45 | - Node 46 | - Yarn / NPM 47 | 48 | ### Domain configuration (first time only) 49 | 50 | Before running the application for the first time, ensure your domain names 51 | point the IP of your Docker daemon by editing your `/etc/hosts` file. 52 | 53 | This IP is probably `127.0.0.1` unless you run Docker in a special VM (like docker-machine for example). 54 | 55 | > [!NOTE] 56 | > The router binds port 80 and 443, that's why it will work with `127.0.0.1` 57 | 58 | ``` 59 | echo '127.0.0.1 db-to-rabbitmq-to-messenger.test' | sudo tee -a /etc/hosts 60 | ``` 61 | 62 | ### Starting the stack 63 | 64 | Launch the stack by running this command: 65 | 66 | ```bash 67 | castor start 68 | ``` 69 | 70 | > [!NOTE] 71 | > the first start of the stack should take a few minutes. 72 | 73 | The site is now accessible at the hostnames your have configured over HTTPS 74 | (you may need to accept self-signed SSL certificate if you do not have mkcert 75 | installed on your computer - see below). 76 | 77 | ### SSL certificates 78 | 79 | HTTPS is supported out of the box. SSL certificates are not versioned and will 80 | be generated the first time you start the infrastructure (`castor start`) or if 81 | you run `castor docker:generate-certificates`. 82 | 83 | If you have `mkcert` installed on your computer, it will be used to generate 84 | locally trusted certificates. See [`mkcert` documentation](https://github.com/FiloSottile/mkcert#installation) 85 | to understand how to install it. Do not forget to install CA root from mkcert 86 | by running `mkcert -install`. 87 | 88 | If you don't have `mkcert`, then self-signed certificates will instead be 89 | generated with openssl. You can configure [infrastructure/docker/services/router/openssl.cnf](infrastructure/docker/services/router/openssl.cnf) 90 | to tweak certificates. 91 | 92 | You can run `castor docker:generate-certificates --force` to recreate new certificates 93 | if some were already generated. Remember to restart the infrastructure to make 94 | use of the new certificates with `castor up` or `castor start`. 95 | 96 | ### Builder 97 | 98 | Having some composer, yarn or other modifications to make on the project? 99 | Start the builder which will give you access to a container with all these 100 | tools available: 101 | 102 | ```bash 103 | castor builder 104 | ``` 105 | 106 | ### Other tasks 107 | 108 | Checkout `castor` to have the list of available tasks. 109 | -------------------------------------------------------------------------------- /infrastructure/docker/services/php/Dockerfile: -------------------------------------------------------------------------------- 1 | # hadolint global ignore=DL3008 2 | 3 | FROM debian:12.6-slim AS php-base 4 | 5 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 6 | 7 | RUN apt-get update \ 8 | && apt-get install -y --no-install-recommends \ 9 | curl \ 10 | ca-certificates \ 11 | gnupg \ 12 | && curl -sSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb \ 13 | && dpkg -i /tmp/debsuryorg-archive-keyring.deb \ 14 | && 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 \ 15 | && apt-get clean \ 16 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* 17 | 18 | RUN apt-get update \ 19 | && apt-get install -y --no-install-recommends \ 20 | bash-completion \ 21 | procps \ 22 | && apt-get clean \ 23 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* 24 | 25 | ARG PHP_VERSION 26 | 27 | RUN apt-get update \ 28 | && apt-get install -y --no-install-recommends \ 29 | "php${PHP_VERSION}-apcu" \ 30 | "php${PHP_VERSION}-amqp" \ 31 | "php${PHP_VERSION}-bcmath" \ 32 | "php${PHP_VERSION}-cli" \ 33 | "php${PHP_VERSION}-common" \ 34 | "php${PHP_VERSION}-curl" \ 35 | "php${PHP_VERSION}-iconv" \ 36 | "php${PHP_VERSION}-intl" \ 37 | "php${PHP_VERSION}-mbstring" \ 38 | "php${PHP_VERSION}-pgsql" \ 39 | "php${PHP_VERSION}-uuid" \ 40 | "php${PHP_VERSION}-xml" \ 41 | "php${PHP_VERSION}-zip" \ 42 | && apt-get clean \ 43 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* 44 | 45 | # Fake user to maps with the one on the host 46 | ARG USER_ID 47 | COPY entrypoint / 48 | RUN addgroup --gid $USER_ID app && \ 49 | adduser --system --uid $USER_ID --home /home/app --shell /bin/bash app && \ 50 | curl -Ls https://github.com/tianon/gosu/releases/download/1.17/gosu-amd64 | \ 51 | install /dev/stdin /usr/local/bin/gosu && \ 52 | sed "s/{{ application_user }}/app/g" -i /entrypoint 53 | 54 | # Configuration 55 | COPY base/php-configuration /etc/php/${PHP_VERSION} 56 | 57 | ENV PHP_VERSION=${PHP_VERSION} 58 | 59 | WORKDIR /var/www 60 | 61 | ENTRYPOINT [ "/entrypoint" ] 62 | 63 | FROM php-base AS frontend 64 | 65 | RUN apt-get update \ 66 | && apt-get install -y --no-install-recommends \ 67 | nginx \ 68 | "php${PHP_VERSION}-fpm" \ 69 | runit \ 70 | && apt-get clean \ 71 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* \ 72 | && rm -r "/etc/php/${PHP_VERSION}/fpm/pool.d/" 73 | 74 | RUN useradd -s /bin/false nginx 75 | 76 | COPY frontend/php-configuration /etc/php/${PHP_VERSION} 77 | COPY frontend/etc/nginx/. /etc/nginx/ 78 | COPY frontend/etc/service/. /etc/service/ 79 | 80 | RUN phpenmod app-default \ 81 | && phpenmod app-fpm 82 | 83 | EXPOSE 80 84 | 85 | CMD ["runsvdir", "-P", "/etc/service"] 86 | 87 | FROM php-base AS worker 88 | 89 | FROM php-base AS builder 90 | 91 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 92 | 93 | ARG NODEJS_VERSION=20.x 94 | RUN curl -s https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /usr/share/keyrings/nodesource.gpg \ 95 | && 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 96 | 97 | # Default toys 98 | RUN apt-get update \ 99 | && apt-get install -y --no-install-recommends \ 100 | git \ 101 | make \ 102 | nodejs \ 103 | sudo \ 104 | unzip \ 105 | && apt-get clean \ 106 | && npm install -g yarn@1.22 \ 107 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* 108 | 109 | # Config 110 | COPY builder/etc/. /etc/ 111 | COPY builder/php-configuration /etc/php/${PHP_VERSION} 112 | RUN adduser app sudo \ 113 | && mkdir /var/log/php \ 114 | && chmod 777 /var/log/php \ 115 | && phpenmod app-default \ 116 | && phpenmod app-builder 117 | 118 | # Composer 119 | COPY --from=composer/composer:2.8.2 /usr/bin/composer /usr/bin/composer 120 | RUN mkdir -p "/home/app/.composer/cache" \ 121 | && chown app: /home/app/.composer -R 122 | 123 | ADD https://raw.githubusercontent.com/symfony/symfony/refs/heads/7.2/src/Symfony/Component/Console/Resources/completion.bash /tmp/completion.bash 124 | 125 | # Composer symfony/console version is too old, and doest not support "API version feature", so we remove it 126 | # Hey, while we are at it, let's add some more completion 127 | RUN sed /tmp/completion.bash \ 128 | -e "s/{{ COMMAND_NAME }}/composer/g" \ 129 | -e 's/"-a{{ VERSION }}"//g' \ 130 | -e "s/{{ VERSION }}/1/g" \ 131 | > /etc/bash_completion.d/composer \ 132 | && sed /tmp/completion.bash \ 133 | -e "s/{{ COMMAND_NAME }}/console/g" \ 134 | -e "s/{{ VERSION }}/1/g" \ 135 | > /etc/bash_completion.d/console 136 | 137 | # Third party tools 138 | ENV PATH="$PATH:/var/www/tools/bin" 139 | 140 | # Good default customization 141 | RUN cat >> /etc/bash.bashrc <title('About this project'); 31 | 32 | io()->comment('Run castor to display all available commands.'); 33 | io()->comment('Run castor about to display this project help.'); 34 | io()->comment('Run castor help [command] to display Castor help.'); 35 | 36 | io()->section('Available URLs for this project:'); 37 | $urls = [variable('root_domain'), ...variable('extra_domains')]; 38 | 39 | try { 40 | $routers = http_client() 41 | ->request('GET', \sprintf('http://%s:8080/api/http/routers', variable('root_domain'))) 42 | ->toArray() 43 | ; 44 | $projectName = variable('project_name'); 45 | foreach ($routers as $router) { 46 | if (!preg_match("{^{$projectName}-(.*)@docker$}", $router['name'])) { 47 | continue; 48 | } 49 | if ("frontend-{$projectName}" === $router['service']) { 50 | continue; 51 | } 52 | if (!preg_match('{^Host\(`(?P.*)`\)$}', $router['rule'], $matches)) { 53 | continue; 54 | } 55 | $hosts = explode('`) || Host(`', $matches['hosts']); 56 | $urls = [...$urls, ...$hosts]; 57 | } 58 | } catch (HttpExceptionInterface) { 59 | } 60 | 61 | io()->listing(array_map(fn ($url) => "https://{$url}", array_unique($urls))); 62 | } 63 | 64 | #[AsTask(description: 'Opens the project in your browser', namespace: '', aliases: ['open'])] 65 | function open_project(): void 66 | { 67 | open('https://' . variable('root_domain')); 68 | } 69 | 70 | #[AsTask(description: 'Builds the infrastructure', aliases: ['build'])] 71 | function build( 72 | ?string $service = null, 73 | ?string $profile = null, 74 | ): void { 75 | io()->title('Building infrastructure'); 76 | 77 | $command = []; 78 | 79 | if ($profile) { 80 | $command[] = '--profile'; 81 | $command[] = $profile; 82 | } else { 83 | $command[] = '--profile'; 84 | $command[] = 'default'; 85 | $command[] = '--profile'; 86 | $command[] = 'worker'; 87 | } 88 | 89 | $command = [ 90 | ...$command, 91 | 'build', 92 | '--build-arg', 'USER_ID=' . variable('user_id'), 93 | '--build-arg', 'PHP_VERSION=' . variable('php_version'), 94 | '--build-arg', 'PROJECT_NAME=' . variable('project_name'), 95 | ]; 96 | 97 | if ($service) { 98 | $command[] = $service; 99 | } 100 | 101 | docker_compose($command, withBuilder: true); 102 | } 103 | 104 | /** 105 | * @param list $profiles 106 | */ 107 | #[AsTask(description: 'Builds and starts the infrastructure', aliases: ['up'])] 108 | function up( 109 | ?string $service = null, 110 | #[AsOption(mode: InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED)] 111 | array $profiles = [], 112 | ): void { 113 | if (!$service && !$profiles) { 114 | io()->title('Starting infrastructure'); 115 | } 116 | 117 | $command = ['up', '--detach', '--wait', '--no-build']; 118 | 119 | if ($service) { 120 | $command[] = $service; 121 | } 122 | 123 | try { 124 | docker_compose($command, profiles: $profiles); 125 | } catch (ExceptionInterface $e) { 126 | io()->error('An error occured while starting the infrastructure.'); 127 | io()->note('Did you forget to run "castor docker:build"?'); 128 | io()->note('Or you forget to login to the registry?'); 129 | 130 | throw $e; 131 | } 132 | } 133 | 134 | /** 135 | * @param list $profiles 136 | */ 137 | #[AsTask(description: 'Stops the infrastructure', aliases: ['stop'])] 138 | function stop( 139 | ?string $service = null, 140 | #[AsOption(mode: InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED)] 141 | array $profiles = [], 142 | ): void { 143 | if (!$service || !$profiles) { 144 | io()->title('Stopping infrastructure'); 145 | } 146 | 147 | $command = ['stop']; 148 | 149 | if ($service) { 150 | $command[] = $service; 151 | } 152 | 153 | docker_compose($command, profiles: $profiles); 154 | } 155 | 156 | #[AsTask(description: 'Opens a shell (bash) into a builder container', aliases: ['builder'])] 157 | function builder(): void 158 | { 159 | $c = context() 160 | ->withTimeout(null) 161 | ->withTty() 162 | ->withEnvironment($_ENV + $_SERVER) 163 | ->withAllowFailure() 164 | ; 165 | docker_compose_run('bash', c: $c); 166 | } 167 | 168 | /** 169 | * @param list $profiles 170 | */ 171 | #[AsTask(description: 'Displays infrastructure logs', aliases: ['logs'])] 172 | function logs( 173 | ?string $service = null, 174 | #[AsOption(mode: InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED)] 175 | array $profiles = [], 176 | ): void { 177 | $command = ['logs', '-f', '--tail', '150']; 178 | 179 | if ($service) { 180 | $command[] = $service; 181 | } 182 | 183 | docker_compose($command, c: context()->withTty(), profiles: $profiles); 184 | } 185 | 186 | #[AsTask(description: 'Lists containers status', aliases: ['ps'])] 187 | function ps(): void 188 | { 189 | docker_compose(['ps'], withBuilder: false); 190 | } 191 | 192 | #[AsTask(description: 'Cleans the infrastructure (remove container, volume, networks)', aliases: ['destroy'])] 193 | function destroy( 194 | #[AsOption(description: 'Force the destruction without confirmation', shortcut: 'f')] 195 | bool $force = false, 196 | ): void { 197 | io()->title('Destroying infrastructure'); 198 | 199 | if (!$force) { 200 | io()->warning('This will permanently remove all containers, volumes, networks... created for this project.'); 201 | io()->note('You can use the --force option to avoid this confirmation.'); 202 | if (!io()->confirm('Are you sure?', false)) { 203 | io()->comment('Aborted.'); 204 | 205 | return; 206 | } 207 | } 208 | 209 | docker_compose(['down', '--remove-orphans', '--volumes', '--rmi=local'], withBuilder: true); 210 | $files = finder() 211 | ->in(variable('root_dir') . '/infrastructure/docker/services/router/certs/') 212 | ->name('*.pem') 213 | ->files() 214 | ; 215 | fs()->remove($files); 216 | } 217 | 218 | #[AsTask(description: 'Generates SSL certificates (with mkcert if available or self-signed if not)')] 219 | function generate_certificates( 220 | #[AsOption(description: 'Force the certificates re-generation without confirmation', shortcut: 'f')] 221 | bool $force = false, 222 | ): void { 223 | $sslDir = variable('root_dir') . '/infrastructure/docker/services/router/certs'; 224 | 225 | if (file_exists("{$sslDir}/cert.pem") && !$force) { 226 | io()->comment('SSL certificates already exists.'); 227 | io()->note('Run "castor docker:generate-certificates --force" to generate new certificates.'); 228 | 229 | return; 230 | } 231 | 232 | io()->title('Generating SSL certificates'); 233 | 234 | if ($force) { 235 | if (file_exists($f = "{$sslDir}/cert.pem")) { 236 | io()->comment('Removing existing certificates in infrastructure/docker/services/router/certs/*.pem.'); 237 | unlink($f); 238 | } 239 | 240 | if (file_exists($f = "{$sslDir}/key.pem")) { 241 | unlink($f); 242 | } 243 | } 244 | 245 | $finder = new ExecutableFinder(); 246 | $mkcert = $finder->find('mkcert'); 247 | 248 | if ($mkcert) { 249 | $pathCaRoot = capture(['mkcert', '-CAROOT']); 250 | 251 | if (!is_dir($pathCaRoot)) { 252 | io()->warning('You must have mkcert CA Root installed on your host with "mkcert -install" command.'); 253 | 254 | return; 255 | } 256 | 257 | $rootDomain = variable('root_domain'); 258 | 259 | run([ 260 | 'mkcert', 261 | '-cert-file', "{$sslDir}/cert.pem", 262 | '-key-file', "{$sslDir}/key.pem", 263 | $rootDomain, 264 | "*.{$rootDomain}", 265 | ...variable('extra_domains'), 266 | ]); 267 | 268 | io()->success('Successfully generated SSL certificates with mkcert.'); 269 | 270 | if ($force) { 271 | io()->note('Please restart the infrastructure to use the new certificates with "castor up" or "castor start".'); 272 | } 273 | 274 | return; 275 | } 276 | 277 | run(['infrastructure/docker/services/router/generate-ssl.sh'], context: context()->withQuiet()); 278 | 279 | io()->success('Successfully generated self-signed SSL certificates in infrastructure/docker/services/router/certs/*.pem.'); 280 | io()->comment('Consider installing mkcert to generate locally trusted SSL certificates and run "castor docker:generate-certificates --force".'); 281 | 282 | if ($force) { 283 | io()->note('Please restart the infrastructure to use the new certificates with "castor up" or "castor start".'); 284 | } 285 | } 286 | 287 | #[AsTask(description: 'Starts the workers', namespace: 'docker:worker', name: 'start', aliases: ['start-workers'])] 288 | function workers_start(): void 289 | { 290 | io()->title('Starting workers'); 291 | 292 | up(profiles: ['worker']); 293 | } 294 | 295 | #[AsTask(description: 'Stops the workers', namespace: 'docker:worker', name: 'stop', aliases: ['stop-workers'])] 296 | function workers_stop(): void 297 | { 298 | io()->title('Stopping workers'); 299 | 300 | stop(profiles: ['worker']); 301 | } 302 | 303 | #[AsContext(default: true)] 304 | function create_default_context(): Context 305 | { 306 | $data = create_default_variables() + [ 307 | 'project_name' => 'app', 308 | 'root_domain' => 'app.test', 309 | 'extra_domains' => [], 310 | 'php_version' => '8.2', 311 | 'docker_compose_files' => [ 312 | 'docker-compose.yml', 313 | ], 314 | 'macos' => false, 315 | 'power_shell' => false, 316 | // check if posix_geteuid is available, if not, use getmyuid (windows) 317 | 'user_id' => \function_exists('posix_geteuid') ? posix_geteuid() : getmyuid(), 318 | 'root_dir' => \dirname(__DIR__), 319 | ]; 320 | 321 | if (file_exists($data['root_dir'] . '/infrastructure/docker/docker-compose.override.yml')) { 322 | $data['docker_compose_files'][] = 'docker-compose.override.yml'; 323 | } 324 | 325 | // We need an empty context to run command, since the default context has 326 | // not been set in castor, since we ARE creating it right now 327 | $emptyContext = new Context(); 328 | 329 | $data['composer_cache_dir'] = cache('composer_cache_dir', function () use ($emptyContext): string { 330 | $composerCacheDir = capture(['composer', 'global', 'config', 'cache-dir', '-q'], onFailure: '', context: $emptyContext); 331 | // If PHP is broken, the output will not be a valid path but an error message 332 | if (!is_dir($composerCacheDir)) { 333 | $composerCacheDir = sys_get_temp_dir() . '/castor/composer'; 334 | // If the directory does not exist, we create it. Otherwise, docker 335 | // will do, as root, and the user will not be able to write in it. 336 | if (!is_dir($composerCacheDir)) { 337 | mkdir($composerCacheDir, 0o777, true); 338 | } 339 | } 340 | 341 | return $composerCacheDir; 342 | }); 343 | 344 | $platform = strtolower(php_uname('s')); 345 | if (str_contains($platform, 'darwin')) { 346 | $data['macos'] = true; 347 | } elseif (\in_array($platform, ['win32', 'win64', 'windows nt'])) { 348 | $data['power_shell'] = true; 349 | } 350 | 351 | if ($data['user_id'] > 256000) { 352 | $data['user_id'] = 1000; 353 | } 354 | 355 | if (0 === $data['user_id']) { 356 | log('Running as root? Fallback to fake user id.', 'warning'); 357 | $data['user_id'] = 1000; 358 | } 359 | 360 | return new Context( 361 | $data, 362 | pty: Process::isPtySupported(), 363 | environment: [ 364 | 'BUILDKIT_PROGRESS' => 'plain', 365 | ] 366 | ); 367 | } 368 | 369 | #[AsContext(name: 'ci')] 370 | function create_ci_context(): Context 371 | { 372 | $c = create_default_context(); 373 | 374 | return $c 375 | ->withData([ 376 | // override the default context here 377 | ]) 378 | ->withEnvironment([ 379 | 'COMPOSE_ANSI' => 'never', 380 | ]) 381 | ; 382 | } 383 | 384 | /** 385 | * @param list $subCommand 386 | * @param list $profiles 387 | */ 388 | function docker_compose(array $subCommand, ?Context $c = null, bool $withBuilder = false, array $profiles = []): Process 389 | { 390 | $c ??= context(); 391 | $profiles = $profiles ?: ['default']; 392 | 393 | $domains = [variable('root_domain'), ...variable('extra_domains')]; 394 | $domains = '`' . implode('`) || Host(`', $domains) . '`'; 395 | 396 | $c = $c 397 | ->withTimeout(null) 398 | ->withEnvironment([ 399 | 'PROJECT_NAME' => variable('project_name'), 400 | 'PROJECT_ROOT_DOMAIN' => variable('root_domain'), 401 | 'PROJECT_DOMAINS' => $domains, 402 | 'USER_ID' => variable('user_id'), 403 | 'COMPOSER_CACHE_DIR' => variable('composer_cache_dir'), 404 | 'PHP_VERSION' => variable('php_version'), 405 | ]) 406 | ; 407 | 408 | $command = [ 409 | 'docker', 410 | 'compose', 411 | '-p', variable('project_name'), 412 | ]; 413 | foreach ($profiles as $profile) { 414 | $command[] = '--profile'; 415 | $command[] = $profile; 416 | } 417 | 418 | foreach (variable('docker_compose_files') as $file) { 419 | $command[] = '-f'; 420 | $command[] = variable('root_dir') . '/infrastructure/docker/' . $file; 421 | } 422 | 423 | if ($withBuilder) { 424 | $command[] = '-f'; 425 | $command[] = variable('root_dir') . '/infrastructure/docker/docker-compose.builder.yml'; 426 | } 427 | 428 | $command = array_merge($command, $subCommand); 429 | 430 | return run($command, context: $c); 431 | } 432 | 433 | function docker_compose_run( 434 | string $runCommand, 435 | ?Context $c = null, 436 | string $service = 'builder', 437 | bool $noDeps = true, 438 | ?string $workDir = null, 439 | bool $portMapping = false, 440 | bool $withBuilder = true, 441 | ): Process { 442 | $command = [ 443 | 'run', 444 | '--rm', 445 | ]; 446 | 447 | if ($noDeps) { 448 | $command[] = '--no-deps'; 449 | } 450 | 451 | if ($portMapping) { 452 | $command[] = '--service-ports'; 453 | } 454 | 455 | if (null !== $workDir) { 456 | $command[] = '-w'; 457 | $command[] = $workDir; 458 | } 459 | 460 | $command[] = $service; 461 | $command[] = '/bin/bash'; 462 | $command[] = '-c'; 463 | $command[] = "{$runCommand}"; 464 | 465 | return docker_compose($command, c: $c, withBuilder: $withBuilder); 466 | } 467 | 468 | function docker_exit_code( 469 | string $runCommand, 470 | ?Context $c = null, 471 | string $service = 'builder', 472 | bool $noDeps = true, 473 | ?string $workDir = null, 474 | bool $withBuilder = true, 475 | ): int { 476 | $c = ($c ?? context())->withAllowFailure(); 477 | 478 | $process = docker_compose_run( 479 | runCommand: $runCommand, 480 | c: $c, 481 | service: $service, 482 | noDeps: $noDeps, 483 | workDir: $workDir, 484 | withBuilder: $withBuilder, 485 | ); 486 | 487 | return $process->getExitCode() ?? 0; 488 | } 489 | 490 | // Mac users have a lot of problems running Yarn / Webpack on the Docker stack 491 | // so this func allow them to run these tools on their host 492 | function run_in_docker_or_locally_for_mac(string $command, ?Context $c = null): void 493 | { 494 | $c ??= context(); 495 | 496 | if (variable('macos')) { 497 | run($command, context: $c->withPath(variable('root_dir'))); 498 | } else { 499 | docker_compose_run($command, c: $c); 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /tools/php-cs-fixer/composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "7f2b0362a40dad2f33333dcca9c01780", 8 | "packages": [ 9 | { 10 | "name": "clue/ndjson-react", 11 | "version": "v1.3.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/clue/reactphp-ndjson.git", 15 | "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", 20 | "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=5.3", 25 | "react/stream": "^1.2" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", 29 | "react/event-loop": "^1.2" 30 | }, 31 | "type": "library", 32 | "autoload": { 33 | "psr-4": { 34 | "Clue\\React\\NDJson\\": "src/" 35 | } 36 | }, 37 | "notification-url": "https://packagist.org/downloads/", 38 | "license": [ 39 | "MIT" 40 | ], 41 | "authors": [ 42 | { 43 | "name": "Christian Lück", 44 | "email": "christian@clue.engineering" 45 | } 46 | ], 47 | "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", 48 | "homepage": "https://github.com/clue/reactphp-ndjson", 49 | "keywords": [ 50 | "NDJSON", 51 | "json", 52 | "jsonlines", 53 | "newline", 54 | "reactphp", 55 | "streaming" 56 | ], 57 | "support": { 58 | "issues": "https://github.com/clue/reactphp-ndjson/issues", 59 | "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" 60 | }, 61 | "funding": [ 62 | { 63 | "url": "https://clue.engineering/support", 64 | "type": "custom" 65 | }, 66 | { 67 | "url": "https://github.com/clue", 68 | "type": "github" 69 | } 70 | ], 71 | "time": "2022-12-23T10:58:28+00:00" 72 | }, 73 | { 74 | "name": "composer/pcre", 75 | "version": "3.3.1", 76 | "source": { 77 | "type": "git", 78 | "url": "https://github.com/composer/pcre.git", 79 | "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4" 80 | }, 81 | "dist": { 82 | "type": "zip", 83 | "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4", 84 | "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4", 85 | "shasum": "" 86 | }, 87 | "require": { 88 | "php": "^7.4 || ^8.0" 89 | }, 90 | "conflict": { 91 | "phpstan/phpstan": "<1.11.10" 92 | }, 93 | "require-dev": { 94 | "phpstan/phpstan": "^1.11.10", 95 | "phpstan/phpstan-strict-rules": "^1.1", 96 | "phpunit/phpunit": "^8 || ^9" 97 | }, 98 | "type": "library", 99 | "extra": { 100 | "branch-alias": { 101 | "dev-main": "3.x-dev" 102 | }, 103 | "phpstan": { 104 | "includes": [ 105 | "extension.neon" 106 | ] 107 | } 108 | }, 109 | "autoload": { 110 | "psr-4": { 111 | "Composer\\Pcre\\": "src" 112 | } 113 | }, 114 | "notification-url": "https://packagist.org/downloads/", 115 | "license": [ 116 | "MIT" 117 | ], 118 | "authors": [ 119 | { 120 | "name": "Jordi Boggiano", 121 | "email": "j.boggiano@seld.be", 122 | "homepage": "http://seld.be" 123 | } 124 | ], 125 | "description": "PCRE wrapping library that offers type-safe preg_* replacements.", 126 | "keywords": [ 127 | "PCRE", 128 | "preg", 129 | "regex", 130 | "regular expression" 131 | ], 132 | "support": { 133 | "issues": "https://github.com/composer/pcre/issues", 134 | "source": "https://github.com/composer/pcre/tree/3.3.1" 135 | }, 136 | "funding": [ 137 | { 138 | "url": "https://packagist.com", 139 | "type": "custom" 140 | }, 141 | { 142 | "url": "https://github.com/composer", 143 | "type": "github" 144 | }, 145 | { 146 | "url": "https://tidelift.com/funding/github/packagist/composer/composer", 147 | "type": "tidelift" 148 | } 149 | ], 150 | "time": "2024-08-27T18:44:43+00:00" 151 | }, 152 | { 153 | "name": "composer/semver", 154 | "version": "3.4.3", 155 | "source": { 156 | "type": "git", 157 | "url": "https://github.com/composer/semver.git", 158 | "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" 159 | }, 160 | "dist": { 161 | "type": "zip", 162 | "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", 163 | "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", 164 | "shasum": "" 165 | }, 166 | "require": { 167 | "php": "^5.3.2 || ^7.0 || ^8.0" 168 | }, 169 | "require-dev": { 170 | "phpstan/phpstan": "^1.11", 171 | "symfony/phpunit-bridge": "^3 || ^7" 172 | }, 173 | "type": "library", 174 | "extra": { 175 | "branch-alias": { 176 | "dev-main": "3.x-dev" 177 | } 178 | }, 179 | "autoload": { 180 | "psr-4": { 181 | "Composer\\Semver\\": "src" 182 | } 183 | }, 184 | "notification-url": "https://packagist.org/downloads/", 185 | "license": [ 186 | "MIT" 187 | ], 188 | "authors": [ 189 | { 190 | "name": "Nils Adermann", 191 | "email": "naderman@naderman.de", 192 | "homepage": "http://www.naderman.de" 193 | }, 194 | { 195 | "name": "Jordi Boggiano", 196 | "email": "j.boggiano@seld.be", 197 | "homepage": "http://seld.be" 198 | }, 199 | { 200 | "name": "Rob Bast", 201 | "email": "rob.bast@gmail.com", 202 | "homepage": "http://robbast.nl" 203 | } 204 | ], 205 | "description": "Semver library that offers utilities, version constraint parsing and validation.", 206 | "keywords": [ 207 | "semantic", 208 | "semver", 209 | "validation", 210 | "versioning" 211 | ], 212 | "support": { 213 | "irc": "ircs://irc.libera.chat:6697/composer", 214 | "issues": "https://github.com/composer/semver/issues", 215 | "source": "https://github.com/composer/semver/tree/3.4.3" 216 | }, 217 | "funding": [ 218 | { 219 | "url": "https://packagist.com", 220 | "type": "custom" 221 | }, 222 | { 223 | "url": "https://github.com/composer", 224 | "type": "github" 225 | }, 226 | { 227 | "url": "https://tidelift.com/funding/github/packagist/composer/composer", 228 | "type": "tidelift" 229 | } 230 | ], 231 | "time": "2024-09-19T14:15:21+00:00" 232 | }, 233 | { 234 | "name": "composer/xdebug-handler", 235 | "version": "3.0.5", 236 | "source": { 237 | "type": "git", 238 | "url": "https://github.com/composer/xdebug-handler.git", 239 | "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" 240 | }, 241 | "dist": { 242 | "type": "zip", 243 | "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", 244 | "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", 245 | "shasum": "" 246 | }, 247 | "require": { 248 | "composer/pcre": "^1 || ^2 || ^3", 249 | "php": "^7.2.5 || ^8.0", 250 | "psr/log": "^1 || ^2 || ^3" 251 | }, 252 | "require-dev": { 253 | "phpstan/phpstan": "^1.0", 254 | "phpstan/phpstan-strict-rules": "^1.1", 255 | "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" 256 | }, 257 | "type": "library", 258 | "autoload": { 259 | "psr-4": { 260 | "Composer\\XdebugHandler\\": "src" 261 | } 262 | }, 263 | "notification-url": "https://packagist.org/downloads/", 264 | "license": [ 265 | "MIT" 266 | ], 267 | "authors": [ 268 | { 269 | "name": "John Stevenson", 270 | "email": "john-stevenson@blueyonder.co.uk" 271 | } 272 | ], 273 | "description": "Restarts a process without Xdebug.", 274 | "keywords": [ 275 | "Xdebug", 276 | "performance" 277 | ], 278 | "support": { 279 | "irc": "ircs://irc.libera.chat:6697/composer", 280 | "issues": "https://github.com/composer/xdebug-handler/issues", 281 | "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" 282 | }, 283 | "funding": [ 284 | { 285 | "url": "https://packagist.com", 286 | "type": "custom" 287 | }, 288 | { 289 | "url": "https://github.com/composer", 290 | "type": "github" 291 | }, 292 | { 293 | "url": "https://tidelift.com/funding/github/packagist/composer/composer", 294 | "type": "tidelift" 295 | } 296 | ], 297 | "time": "2024-05-06T16:37:16+00:00" 298 | }, 299 | { 300 | "name": "evenement/evenement", 301 | "version": "v3.0.2", 302 | "source": { 303 | "type": "git", 304 | "url": "https://github.com/igorw/evenement.git", 305 | "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" 306 | }, 307 | "dist": { 308 | "type": "zip", 309 | "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", 310 | "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", 311 | "shasum": "" 312 | }, 313 | "require": { 314 | "php": ">=7.0" 315 | }, 316 | "require-dev": { 317 | "phpunit/phpunit": "^9 || ^6" 318 | }, 319 | "type": "library", 320 | "autoload": { 321 | "psr-4": { 322 | "Evenement\\": "src/" 323 | } 324 | }, 325 | "notification-url": "https://packagist.org/downloads/", 326 | "license": [ 327 | "MIT" 328 | ], 329 | "authors": [ 330 | { 331 | "name": "Igor Wiedler", 332 | "email": "igor@wiedler.ch" 333 | } 334 | ], 335 | "description": "Événement is a very simple event dispatching library for PHP", 336 | "keywords": [ 337 | "event-dispatcher", 338 | "event-emitter" 339 | ], 340 | "support": { 341 | "issues": "https://github.com/igorw/evenement/issues", 342 | "source": "https://github.com/igorw/evenement/tree/v3.0.2" 343 | }, 344 | "time": "2023-08-08T05:53:35+00:00" 345 | }, 346 | { 347 | "name": "fidry/cpu-core-counter", 348 | "version": "1.2.0", 349 | "source": { 350 | "type": "git", 351 | "url": "https://github.com/theofidry/cpu-core-counter.git", 352 | "reference": "8520451a140d3f46ac33042715115e290cf5785f" 353 | }, 354 | "dist": { 355 | "type": "zip", 356 | "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", 357 | "reference": "8520451a140d3f46ac33042715115e290cf5785f", 358 | "shasum": "" 359 | }, 360 | "require": { 361 | "php": "^7.2 || ^8.0" 362 | }, 363 | "require-dev": { 364 | "fidry/makefile": "^0.2.0", 365 | "fidry/php-cs-fixer-config": "^1.1.2", 366 | "phpstan/extension-installer": "^1.2.0", 367 | "phpstan/phpstan": "^1.9.2", 368 | "phpstan/phpstan-deprecation-rules": "^1.0.0", 369 | "phpstan/phpstan-phpunit": "^1.2.2", 370 | "phpstan/phpstan-strict-rules": "^1.4.4", 371 | "phpunit/phpunit": "^8.5.31 || ^9.5.26", 372 | "webmozarts/strict-phpunit": "^7.5" 373 | }, 374 | "type": "library", 375 | "autoload": { 376 | "psr-4": { 377 | "Fidry\\CpuCoreCounter\\": "src/" 378 | } 379 | }, 380 | "notification-url": "https://packagist.org/downloads/", 381 | "license": [ 382 | "MIT" 383 | ], 384 | "authors": [ 385 | { 386 | "name": "Théo FIDRY", 387 | "email": "theo.fidry@gmail.com" 388 | } 389 | ], 390 | "description": "Tiny utility to get the number of CPU cores.", 391 | "keywords": [ 392 | "CPU", 393 | "core" 394 | ], 395 | "support": { 396 | "issues": "https://github.com/theofidry/cpu-core-counter/issues", 397 | "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" 398 | }, 399 | "funding": [ 400 | { 401 | "url": "https://github.com/theofidry", 402 | "type": "github" 403 | } 404 | ], 405 | "time": "2024-08-06T10:04:20+00:00" 406 | }, 407 | { 408 | "name": "friendsofphp/php-cs-fixer", 409 | "version": "v3.64.0", 410 | "source": { 411 | "type": "git", 412 | "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", 413 | "reference": "58dd9c931c785a79739310aef5178928305ffa67" 414 | }, 415 | "dist": { 416 | "type": "zip", 417 | "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/58dd9c931c785a79739310aef5178928305ffa67", 418 | "reference": "58dd9c931c785a79739310aef5178928305ffa67", 419 | "shasum": "" 420 | }, 421 | "require": { 422 | "clue/ndjson-react": "^1.0", 423 | "composer/semver": "^3.4", 424 | "composer/xdebug-handler": "^3.0.3", 425 | "ext-filter": "*", 426 | "ext-json": "*", 427 | "ext-tokenizer": "*", 428 | "fidry/cpu-core-counter": "^1.0", 429 | "php": "^7.4 || ^8.0", 430 | "react/child-process": "^0.6.5", 431 | "react/event-loop": "^1.0", 432 | "react/promise": "^2.0 || ^3.0", 433 | "react/socket": "^1.0", 434 | "react/stream": "^1.0", 435 | "sebastian/diff": "^4.0 || ^5.0 || ^6.0", 436 | "symfony/console": "^5.4 || ^6.0 || ^7.0", 437 | "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", 438 | "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", 439 | "symfony/finder": "^5.4 || ^6.0 || ^7.0", 440 | "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0", 441 | "symfony/polyfill-mbstring": "^1.28", 442 | "symfony/polyfill-php80": "^1.28", 443 | "symfony/polyfill-php81": "^1.28", 444 | "symfony/process": "^5.4 || ^6.0 || ^7.0", 445 | "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" 446 | }, 447 | "require-dev": { 448 | "facile-it/paraunit": "^1.3 || ^2.3", 449 | "infection/infection": "^0.29.5", 450 | "justinrainbow/json-schema": "^5.2", 451 | "keradus/cli-executor": "^2.1", 452 | "mikey179/vfsstream": "^1.6.11", 453 | "php-coveralls/php-coveralls": "^2.7", 454 | "php-cs-fixer/accessible-object": "^1.1", 455 | "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5", 456 | "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5", 457 | "phpunit/phpunit": "^9.6.19 || ^10.5.21 || ^11.2", 458 | "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0", 459 | "symfony/yaml": "^5.4 || ^6.0 || ^7.0" 460 | }, 461 | "suggest": { 462 | "ext-dom": "For handling output formats in XML", 463 | "ext-mbstring": "For handling non-UTF8 characters." 464 | }, 465 | "bin": [ 466 | "php-cs-fixer" 467 | ], 468 | "type": "application", 469 | "autoload": { 470 | "psr-4": { 471 | "PhpCsFixer\\": "src/" 472 | }, 473 | "exclude-from-classmap": [ 474 | "src/Fixer/Internal/*" 475 | ] 476 | }, 477 | "notification-url": "https://packagist.org/downloads/", 478 | "license": [ 479 | "MIT" 480 | ], 481 | "authors": [ 482 | { 483 | "name": "Fabien Potencier", 484 | "email": "fabien@symfony.com" 485 | }, 486 | { 487 | "name": "Dariusz Rumiński", 488 | "email": "dariusz.ruminski@gmail.com" 489 | } 490 | ], 491 | "description": "A tool to automatically fix PHP code style", 492 | "keywords": [ 493 | "Static code analysis", 494 | "fixer", 495 | "standards", 496 | "static analysis" 497 | ], 498 | "support": { 499 | "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", 500 | "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.64.0" 501 | }, 502 | "funding": [ 503 | { 504 | "url": "https://github.com/keradus", 505 | "type": "github" 506 | } 507 | ], 508 | "time": "2024-08-30T23:09:38+00:00" 509 | }, 510 | { 511 | "name": "psr/container", 512 | "version": "2.0.2", 513 | "source": { 514 | "type": "git", 515 | "url": "https://github.com/php-fig/container.git", 516 | "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" 517 | }, 518 | "dist": { 519 | "type": "zip", 520 | "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", 521 | "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", 522 | "shasum": "" 523 | }, 524 | "require": { 525 | "php": ">=7.4.0" 526 | }, 527 | "type": "library", 528 | "extra": { 529 | "branch-alias": { 530 | "dev-master": "2.0.x-dev" 531 | } 532 | }, 533 | "autoload": { 534 | "psr-4": { 535 | "Psr\\Container\\": "src/" 536 | } 537 | }, 538 | "notification-url": "https://packagist.org/downloads/", 539 | "license": [ 540 | "MIT" 541 | ], 542 | "authors": [ 543 | { 544 | "name": "PHP-FIG", 545 | "homepage": "https://www.php-fig.org/" 546 | } 547 | ], 548 | "description": "Common Container Interface (PHP FIG PSR-11)", 549 | "homepage": "https://github.com/php-fig/container", 550 | "keywords": [ 551 | "PSR-11", 552 | "container", 553 | "container-interface", 554 | "container-interop", 555 | "psr" 556 | ], 557 | "support": { 558 | "issues": "https://github.com/php-fig/container/issues", 559 | "source": "https://github.com/php-fig/container/tree/2.0.2" 560 | }, 561 | "time": "2021-11-05T16:47:00+00:00" 562 | }, 563 | { 564 | "name": "psr/event-dispatcher", 565 | "version": "1.0.0", 566 | "source": { 567 | "type": "git", 568 | "url": "https://github.com/php-fig/event-dispatcher.git", 569 | "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" 570 | }, 571 | "dist": { 572 | "type": "zip", 573 | "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", 574 | "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", 575 | "shasum": "" 576 | }, 577 | "require": { 578 | "php": ">=7.2.0" 579 | }, 580 | "type": "library", 581 | "extra": { 582 | "branch-alias": { 583 | "dev-master": "1.0.x-dev" 584 | } 585 | }, 586 | "autoload": { 587 | "psr-4": { 588 | "Psr\\EventDispatcher\\": "src/" 589 | } 590 | }, 591 | "notification-url": "https://packagist.org/downloads/", 592 | "license": [ 593 | "MIT" 594 | ], 595 | "authors": [ 596 | { 597 | "name": "PHP-FIG", 598 | "homepage": "http://www.php-fig.org/" 599 | } 600 | ], 601 | "description": "Standard interfaces for event handling.", 602 | "keywords": [ 603 | "events", 604 | "psr", 605 | "psr-14" 606 | ], 607 | "support": { 608 | "issues": "https://github.com/php-fig/event-dispatcher/issues", 609 | "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" 610 | }, 611 | "time": "2019-01-08T18:20:26+00:00" 612 | }, 613 | { 614 | "name": "psr/log", 615 | "version": "3.0.2", 616 | "source": { 617 | "type": "git", 618 | "url": "https://github.com/php-fig/log.git", 619 | "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" 620 | }, 621 | "dist": { 622 | "type": "zip", 623 | "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", 624 | "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", 625 | "shasum": "" 626 | }, 627 | "require": { 628 | "php": ">=8.0.0" 629 | }, 630 | "type": "library", 631 | "extra": { 632 | "branch-alias": { 633 | "dev-master": "3.x-dev" 634 | } 635 | }, 636 | "autoload": { 637 | "psr-4": { 638 | "Psr\\Log\\": "src" 639 | } 640 | }, 641 | "notification-url": "https://packagist.org/downloads/", 642 | "license": [ 643 | "MIT" 644 | ], 645 | "authors": [ 646 | { 647 | "name": "PHP-FIG", 648 | "homepage": "https://www.php-fig.org/" 649 | } 650 | ], 651 | "description": "Common interface for logging libraries", 652 | "homepage": "https://github.com/php-fig/log", 653 | "keywords": [ 654 | "log", 655 | "psr", 656 | "psr-3" 657 | ], 658 | "support": { 659 | "source": "https://github.com/php-fig/log/tree/3.0.2" 660 | }, 661 | "time": "2024-09-11T13:17:53+00:00" 662 | }, 663 | { 664 | "name": "react/cache", 665 | "version": "v1.2.0", 666 | "source": { 667 | "type": "git", 668 | "url": "https://github.com/reactphp/cache.git", 669 | "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" 670 | }, 671 | "dist": { 672 | "type": "zip", 673 | "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", 674 | "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", 675 | "shasum": "" 676 | }, 677 | "require": { 678 | "php": ">=5.3.0", 679 | "react/promise": "^3.0 || ^2.0 || ^1.1" 680 | }, 681 | "require-dev": { 682 | "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" 683 | }, 684 | "type": "library", 685 | "autoload": { 686 | "psr-4": { 687 | "React\\Cache\\": "src/" 688 | } 689 | }, 690 | "notification-url": "https://packagist.org/downloads/", 691 | "license": [ 692 | "MIT" 693 | ], 694 | "authors": [ 695 | { 696 | "name": "Christian Lück", 697 | "email": "christian@clue.engineering", 698 | "homepage": "https://clue.engineering/" 699 | }, 700 | { 701 | "name": "Cees-Jan Kiewiet", 702 | "email": "reactphp@ceesjankiewiet.nl", 703 | "homepage": "https://wyrihaximus.net/" 704 | }, 705 | { 706 | "name": "Jan Sorgalla", 707 | "email": "jsorgalla@gmail.com", 708 | "homepage": "https://sorgalla.com/" 709 | }, 710 | { 711 | "name": "Chris Boden", 712 | "email": "cboden@gmail.com", 713 | "homepage": "https://cboden.dev/" 714 | } 715 | ], 716 | "description": "Async, Promise-based cache interface for ReactPHP", 717 | "keywords": [ 718 | "cache", 719 | "caching", 720 | "promise", 721 | "reactphp" 722 | ], 723 | "support": { 724 | "issues": "https://github.com/reactphp/cache/issues", 725 | "source": "https://github.com/reactphp/cache/tree/v1.2.0" 726 | }, 727 | "funding": [ 728 | { 729 | "url": "https://opencollective.com/reactphp", 730 | "type": "open_collective" 731 | } 732 | ], 733 | "time": "2022-11-30T15:59:55+00:00" 734 | }, 735 | { 736 | "name": "react/child-process", 737 | "version": "v0.6.5", 738 | "source": { 739 | "type": "git", 740 | "url": "https://github.com/reactphp/child-process.git", 741 | "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43" 742 | }, 743 | "dist": { 744 | "type": "zip", 745 | "url": "https://api.github.com/repos/reactphp/child-process/zipball/e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", 746 | "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", 747 | "shasum": "" 748 | }, 749 | "require": { 750 | "evenement/evenement": "^3.0 || ^2.0 || ^1.0", 751 | "php": ">=5.3.0", 752 | "react/event-loop": "^1.2", 753 | "react/stream": "^1.2" 754 | }, 755 | "require-dev": { 756 | "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", 757 | "react/socket": "^1.8", 758 | "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" 759 | }, 760 | "type": "library", 761 | "autoload": { 762 | "psr-4": { 763 | "React\\ChildProcess\\": "src" 764 | } 765 | }, 766 | "notification-url": "https://packagist.org/downloads/", 767 | "license": [ 768 | "MIT" 769 | ], 770 | "authors": [ 771 | { 772 | "name": "Christian Lück", 773 | "email": "christian@clue.engineering", 774 | "homepage": "https://clue.engineering/" 775 | }, 776 | { 777 | "name": "Cees-Jan Kiewiet", 778 | "email": "reactphp@ceesjankiewiet.nl", 779 | "homepage": "https://wyrihaximus.net/" 780 | }, 781 | { 782 | "name": "Jan Sorgalla", 783 | "email": "jsorgalla@gmail.com", 784 | "homepage": "https://sorgalla.com/" 785 | }, 786 | { 787 | "name": "Chris Boden", 788 | "email": "cboden@gmail.com", 789 | "homepage": "https://cboden.dev/" 790 | } 791 | ], 792 | "description": "Event-driven library for executing child processes with ReactPHP.", 793 | "keywords": [ 794 | "event-driven", 795 | "process", 796 | "reactphp" 797 | ], 798 | "support": { 799 | "issues": "https://github.com/reactphp/child-process/issues", 800 | "source": "https://github.com/reactphp/child-process/tree/v0.6.5" 801 | }, 802 | "funding": [ 803 | { 804 | "url": "https://github.com/WyriHaximus", 805 | "type": "github" 806 | }, 807 | { 808 | "url": "https://github.com/clue", 809 | "type": "github" 810 | } 811 | ], 812 | "time": "2022-09-16T13:41:56+00:00" 813 | }, 814 | { 815 | "name": "react/dns", 816 | "version": "v1.13.0", 817 | "source": { 818 | "type": "git", 819 | "url": "https://github.com/reactphp/dns.git", 820 | "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" 821 | }, 822 | "dist": { 823 | "type": "zip", 824 | "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", 825 | "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", 826 | "shasum": "" 827 | }, 828 | "require": { 829 | "php": ">=5.3.0", 830 | "react/cache": "^1.0 || ^0.6 || ^0.5", 831 | "react/event-loop": "^1.2", 832 | "react/promise": "^3.2 || ^2.7 || ^1.2.1" 833 | }, 834 | "require-dev": { 835 | "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", 836 | "react/async": "^4.3 || ^3 || ^2", 837 | "react/promise-timer": "^1.11" 838 | }, 839 | "type": "library", 840 | "autoload": { 841 | "psr-4": { 842 | "React\\Dns\\": "src/" 843 | } 844 | }, 845 | "notification-url": "https://packagist.org/downloads/", 846 | "license": [ 847 | "MIT" 848 | ], 849 | "authors": [ 850 | { 851 | "name": "Christian Lück", 852 | "email": "christian@clue.engineering", 853 | "homepage": "https://clue.engineering/" 854 | }, 855 | { 856 | "name": "Cees-Jan Kiewiet", 857 | "email": "reactphp@ceesjankiewiet.nl", 858 | "homepage": "https://wyrihaximus.net/" 859 | }, 860 | { 861 | "name": "Jan Sorgalla", 862 | "email": "jsorgalla@gmail.com", 863 | "homepage": "https://sorgalla.com/" 864 | }, 865 | { 866 | "name": "Chris Boden", 867 | "email": "cboden@gmail.com", 868 | "homepage": "https://cboden.dev/" 869 | } 870 | ], 871 | "description": "Async DNS resolver for ReactPHP", 872 | "keywords": [ 873 | "async", 874 | "dns", 875 | "dns-resolver", 876 | "reactphp" 877 | ], 878 | "support": { 879 | "issues": "https://github.com/reactphp/dns/issues", 880 | "source": "https://github.com/reactphp/dns/tree/v1.13.0" 881 | }, 882 | "funding": [ 883 | { 884 | "url": "https://opencollective.com/reactphp", 885 | "type": "open_collective" 886 | } 887 | ], 888 | "time": "2024-06-13T14:18:03+00:00" 889 | }, 890 | { 891 | "name": "react/event-loop", 892 | "version": "v1.5.0", 893 | "source": { 894 | "type": "git", 895 | "url": "https://github.com/reactphp/event-loop.git", 896 | "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" 897 | }, 898 | "dist": { 899 | "type": "zip", 900 | "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", 901 | "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", 902 | "shasum": "" 903 | }, 904 | "require": { 905 | "php": ">=5.3.0" 906 | }, 907 | "require-dev": { 908 | "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" 909 | }, 910 | "suggest": { 911 | "ext-pcntl": "For signal handling support when using the StreamSelectLoop" 912 | }, 913 | "type": "library", 914 | "autoload": { 915 | "psr-4": { 916 | "React\\EventLoop\\": "src/" 917 | } 918 | }, 919 | "notification-url": "https://packagist.org/downloads/", 920 | "license": [ 921 | "MIT" 922 | ], 923 | "authors": [ 924 | { 925 | "name": "Christian Lück", 926 | "email": "christian@clue.engineering", 927 | "homepage": "https://clue.engineering/" 928 | }, 929 | { 930 | "name": "Cees-Jan Kiewiet", 931 | "email": "reactphp@ceesjankiewiet.nl", 932 | "homepage": "https://wyrihaximus.net/" 933 | }, 934 | { 935 | "name": "Jan Sorgalla", 936 | "email": "jsorgalla@gmail.com", 937 | "homepage": "https://sorgalla.com/" 938 | }, 939 | { 940 | "name": "Chris Boden", 941 | "email": "cboden@gmail.com", 942 | "homepage": "https://cboden.dev/" 943 | } 944 | ], 945 | "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", 946 | "keywords": [ 947 | "asynchronous", 948 | "event-loop" 949 | ], 950 | "support": { 951 | "issues": "https://github.com/reactphp/event-loop/issues", 952 | "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" 953 | }, 954 | "funding": [ 955 | { 956 | "url": "https://opencollective.com/reactphp", 957 | "type": "open_collective" 958 | } 959 | ], 960 | "time": "2023-11-13T13:48:05+00:00" 961 | }, 962 | { 963 | "name": "react/promise", 964 | "version": "v3.2.0", 965 | "source": { 966 | "type": "git", 967 | "url": "https://github.com/reactphp/promise.git", 968 | "reference": "8a164643313c71354582dc850b42b33fa12a4b63" 969 | }, 970 | "dist": { 971 | "type": "zip", 972 | "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", 973 | "reference": "8a164643313c71354582dc850b42b33fa12a4b63", 974 | "shasum": "" 975 | }, 976 | "require": { 977 | "php": ">=7.1.0" 978 | }, 979 | "require-dev": { 980 | "phpstan/phpstan": "1.10.39 || 1.4.10", 981 | "phpunit/phpunit": "^9.6 || ^7.5" 982 | }, 983 | "type": "library", 984 | "autoload": { 985 | "files": [ 986 | "src/functions_include.php" 987 | ], 988 | "psr-4": { 989 | "React\\Promise\\": "src/" 990 | } 991 | }, 992 | "notification-url": "https://packagist.org/downloads/", 993 | "license": [ 994 | "MIT" 995 | ], 996 | "authors": [ 997 | { 998 | "name": "Jan Sorgalla", 999 | "email": "jsorgalla@gmail.com", 1000 | "homepage": "https://sorgalla.com/" 1001 | }, 1002 | { 1003 | "name": "Christian Lück", 1004 | "email": "christian@clue.engineering", 1005 | "homepage": "https://clue.engineering/" 1006 | }, 1007 | { 1008 | "name": "Cees-Jan Kiewiet", 1009 | "email": "reactphp@ceesjankiewiet.nl", 1010 | "homepage": "https://wyrihaximus.net/" 1011 | }, 1012 | { 1013 | "name": "Chris Boden", 1014 | "email": "cboden@gmail.com", 1015 | "homepage": "https://cboden.dev/" 1016 | } 1017 | ], 1018 | "description": "A lightweight implementation of CommonJS Promises/A for PHP", 1019 | "keywords": [ 1020 | "promise", 1021 | "promises" 1022 | ], 1023 | "support": { 1024 | "issues": "https://github.com/reactphp/promise/issues", 1025 | "source": "https://github.com/reactphp/promise/tree/v3.2.0" 1026 | }, 1027 | "funding": [ 1028 | { 1029 | "url": "https://opencollective.com/reactphp", 1030 | "type": "open_collective" 1031 | } 1032 | ], 1033 | "time": "2024-05-24T10:39:05+00:00" 1034 | }, 1035 | { 1036 | "name": "react/socket", 1037 | "version": "v1.16.0", 1038 | "source": { 1039 | "type": "git", 1040 | "url": "https://github.com/reactphp/socket.git", 1041 | "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" 1042 | }, 1043 | "dist": { 1044 | "type": "zip", 1045 | "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", 1046 | "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", 1047 | "shasum": "" 1048 | }, 1049 | "require": { 1050 | "evenement/evenement": "^3.0 || ^2.0 || ^1.0", 1051 | "php": ">=5.3.0", 1052 | "react/dns": "^1.13", 1053 | "react/event-loop": "^1.2", 1054 | "react/promise": "^3.2 || ^2.6 || ^1.2.1", 1055 | "react/stream": "^1.4" 1056 | }, 1057 | "require-dev": { 1058 | "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", 1059 | "react/async": "^4.3 || ^3.3 || ^2", 1060 | "react/promise-stream": "^1.4", 1061 | "react/promise-timer": "^1.11" 1062 | }, 1063 | "type": "library", 1064 | "autoload": { 1065 | "psr-4": { 1066 | "React\\Socket\\": "src/" 1067 | } 1068 | }, 1069 | "notification-url": "https://packagist.org/downloads/", 1070 | "license": [ 1071 | "MIT" 1072 | ], 1073 | "authors": [ 1074 | { 1075 | "name": "Christian Lück", 1076 | "email": "christian@clue.engineering", 1077 | "homepage": "https://clue.engineering/" 1078 | }, 1079 | { 1080 | "name": "Cees-Jan Kiewiet", 1081 | "email": "reactphp@ceesjankiewiet.nl", 1082 | "homepage": "https://wyrihaximus.net/" 1083 | }, 1084 | { 1085 | "name": "Jan Sorgalla", 1086 | "email": "jsorgalla@gmail.com", 1087 | "homepage": "https://sorgalla.com/" 1088 | }, 1089 | { 1090 | "name": "Chris Boden", 1091 | "email": "cboden@gmail.com", 1092 | "homepage": "https://cboden.dev/" 1093 | } 1094 | ], 1095 | "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", 1096 | "keywords": [ 1097 | "Connection", 1098 | "Socket", 1099 | "async", 1100 | "reactphp", 1101 | "stream" 1102 | ], 1103 | "support": { 1104 | "issues": "https://github.com/reactphp/socket/issues", 1105 | "source": "https://github.com/reactphp/socket/tree/v1.16.0" 1106 | }, 1107 | "funding": [ 1108 | { 1109 | "url": "https://opencollective.com/reactphp", 1110 | "type": "open_collective" 1111 | } 1112 | ], 1113 | "time": "2024-07-26T10:38:09+00:00" 1114 | }, 1115 | { 1116 | "name": "react/stream", 1117 | "version": "v1.4.0", 1118 | "source": { 1119 | "type": "git", 1120 | "url": "https://github.com/reactphp/stream.git", 1121 | "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" 1122 | }, 1123 | "dist": { 1124 | "type": "zip", 1125 | "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", 1126 | "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", 1127 | "shasum": "" 1128 | }, 1129 | "require": { 1130 | "evenement/evenement": "^3.0 || ^2.0 || ^1.0", 1131 | "php": ">=5.3.8", 1132 | "react/event-loop": "^1.2" 1133 | }, 1134 | "require-dev": { 1135 | "clue/stream-filter": "~1.2", 1136 | "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" 1137 | }, 1138 | "type": "library", 1139 | "autoload": { 1140 | "psr-4": { 1141 | "React\\Stream\\": "src/" 1142 | } 1143 | }, 1144 | "notification-url": "https://packagist.org/downloads/", 1145 | "license": [ 1146 | "MIT" 1147 | ], 1148 | "authors": [ 1149 | { 1150 | "name": "Christian Lück", 1151 | "email": "christian@clue.engineering", 1152 | "homepage": "https://clue.engineering/" 1153 | }, 1154 | { 1155 | "name": "Cees-Jan Kiewiet", 1156 | "email": "reactphp@ceesjankiewiet.nl", 1157 | "homepage": "https://wyrihaximus.net/" 1158 | }, 1159 | { 1160 | "name": "Jan Sorgalla", 1161 | "email": "jsorgalla@gmail.com", 1162 | "homepage": "https://sorgalla.com/" 1163 | }, 1164 | { 1165 | "name": "Chris Boden", 1166 | "email": "cboden@gmail.com", 1167 | "homepage": "https://cboden.dev/" 1168 | } 1169 | ], 1170 | "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", 1171 | "keywords": [ 1172 | "event-driven", 1173 | "io", 1174 | "non-blocking", 1175 | "pipe", 1176 | "reactphp", 1177 | "readable", 1178 | "stream", 1179 | "writable" 1180 | ], 1181 | "support": { 1182 | "issues": "https://github.com/reactphp/stream/issues", 1183 | "source": "https://github.com/reactphp/stream/tree/v1.4.0" 1184 | }, 1185 | "funding": [ 1186 | { 1187 | "url": "https://opencollective.com/reactphp", 1188 | "type": "open_collective" 1189 | } 1190 | ], 1191 | "time": "2024-06-11T12:45:25+00:00" 1192 | }, 1193 | { 1194 | "name": "sebastian/diff", 1195 | "version": "5.1.1", 1196 | "source": { 1197 | "type": "git", 1198 | "url": "https://github.com/sebastianbergmann/diff.git", 1199 | "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" 1200 | }, 1201 | "dist": { 1202 | "type": "zip", 1203 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", 1204 | "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", 1205 | "shasum": "" 1206 | }, 1207 | "require": { 1208 | "php": ">=8.1" 1209 | }, 1210 | "require-dev": { 1211 | "phpunit/phpunit": "^10.0", 1212 | "symfony/process": "^6.4" 1213 | }, 1214 | "type": "library", 1215 | "extra": { 1216 | "branch-alias": { 1217 | "dev-main": "5.1-dev" 1218 | } 1219 | }, 1220 | "autoload": { 1221 | "classmap": [ 1222 | "src/" 1223 | ] 1224 | }, 1225 | "notification-url": "https://packagist.org/downloads/", 1226 | "license": [ 1227 | "BSD-3-Clause" 1228 | ], 1229 | "authors": [ 1230 | { 1231 | "name": "Sebastian Bergmann", 1232 | "email": "sebastian@phpunit.de" 1233 | }, 1234 | { 1235 | "name": "Kore Nordmann", 1236 | "email": "mail@kore-nordmann.de" 1237 | } 1238 | ], 1239 | "description": "Diff implementation", 1240 | "homepage": "https://github.com/sebastianbergmann/diff", 1241 | "keywords": [ 1242 | "diff", 1243 | "udiff", 1244 | "unidiff", 1245 | "unified diff" 1246 | ], 1247 | "support": { 1248 | "issues": "https://github.com/sebastianbergmann/diff/issues", 1249 | "security": "https://github.com/sebastianbergmann/diff/security/policy", 1250 | "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" 1251 | }, 1252 | "funding": [ 1253 | { 1254 | "url": "https://github.com/sebastianbergmann", 1255 | "type": "github" 1256 | } 1257 | ], 1258 | "time": "2024-03-02T07:15:17+00:00" 1259 | }, 1260 | { 1261 | "name": "symfony/console", 1262 | "version": "v6.4.13", 1263 | "source": { 1264 | "type": "git", 1265 | "url": "https://github.com/symfony/console.git", 1266 | "reference": "f793dd5a7d9ae9923e35d0503d08ba734cec1d79" 1267 | }, 1268 | "dist": { 1269 | "type": "zip", 1270 | "url": "https://api.github.com/repos/symfony/console/zipball/f793dd5a7d9ae9923e35d0503d08ba734cec1d79", 1271 | "reference": "f793dd5a7d9ae9923e35d0503d08ba734cec1d79", 1272 | "shasum": "" 1273 | }, 1274 | "require": { 1275 | "php": ">=8.1", 1276 | "symfony/deprecation-contracts": "^2.5|^3", 1277 | "symfony/polyfill-mbstring": "~1.0", 1278 | "symfony/service-contracts": "^2.5|^3", 1279 | "symfony/string": "^5.4|^6.0|^7.0" 1280 | }, 1281 | "conflict": { 1282 | "symfony/dependency-injection": "<5.4", 1283 | "symfony/dotenv": "<5.4", 1284 | "symfony/event-dispatcher": "<5.4", 1285 | "symfony/lock": "<5.4", 1286 | "symfony/process": "<5.4" 1287 | }, 1288 | "provide": { 1289 | "psr/log-implementation": "1.0|2.0|3.0" 1290 | }, 1291 | "require-dev": { 1292 | "psr/log": "^1|^2|^3", 1293 | "symfony/config": "^5.4|^6.0|^7.0", 1294 | "symfony/dependency-injection": "^5.4|^6.0|^7.0", 1295 | "symfony/event-dispatcher": "^5.4|^6.0|^7.0", 1296 | "symfony/http-foundation": "^6.4|^7.0", 1297 | "symfony/http-kernel": "^6.4|^7.0", 1298 | "symfony/lock": "^5.4|^6.0|^7.0", 1299 | "symfony/messenger": "^5.4|^6.0|^7.0", 1300 | "symfony/process": "^5.4|^6.0|^7.0", 1301 | "symfony/stopwatch": "^5.4|^6.0|^7.0", 1302 | "symfony/var-dumper": "^5.4|^6.0|^7.0" 1303 | }, 1304 | "type": "library", 1305 | "autoload": { 1306 | "psr-4": { 1307 | "Symfony\\Component\\Console\\": "" 1308 | }, 1309 | "exclude-from-classmap": [ 1310 | "/Tests/" 1311 | ] 1312 | }, 1313 | "notification-url": "https://packagist.org/downloads/", 1314 | "license": [ 1315 | "MIT" 1316 | ], 1317 | "authors": [ 1318 | { 1319 | "name": "Fabien Potencier", 1320 | "email": "fabien@symfony.com" 1321 | }, 1322 | { 1323 | "name": "Symfony Community", 1324 | "homepage": "https://symfony.com/contributors" 1325 | } 1326 | ], 1327 | "description": "Eases the creation of beautiful and testable command line interfaces", 1328 | "homepage": "https://symfony.com", 1329 | "keywords": [ 1330 | "cli", 1331 | "command-line", 1332 | "console", 1333 | "terminal" 1334 | ], 1335 | "support": { 1336 | "source": "https://github.com/symfony/console/tree/v6.4.13" 1337 | }, 1338 | "funding": [ 1339 | { 1340 | "url": "https://symfony.com/sponsor", 1341 | "type": "custom" 1342 | }, 1343 | { 1344 | "url": "https://github.com/fabpot", 1345 | "type": "github" 1346 | }, 1347 | { 1348 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1349 | "type": "tidelift" 1350 | } 1351 | ], 1352 | "time": "2024-10-09T08:40:40+00:00" 1353 | }, 1354 | { 1355 | "name": "symfony/deprecation-contracts", 1356 | "version": "v3.5.0", 1357 | "source": { 1358 | "type": "git", 1359 | "url": "https://github.com/symfony/deprecation-contracts.git", 1360 | "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" 1361 | }, 1362 | "dist": { 1363 | "type": "zip", 1364 | "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", 1365 | "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", 1366 | "shasum": "" 1367 | }, 1368 | "require": { 1369 | "php": ">=8.1" 1370 | }, 1371 | "type": "library", 1372 | "extra": { 1373 | "branch-alias": { 1374 | "dev-main": "3.5-dev" 1375 | }, 1376 | "thanks": { 1377 | "name": "symfony/contracts", 1378 | "url": "https://github.com/symfony/contracts" 1379 | } 1380 | }, 1381 | "autoload": { 1382 | "files": [ 1383 | "function.php" 1384 | ] 1385 | }, 1386 | "notification-url": "https://packagist.org/downloads/", 1387 | "license": [ 1388 | "MIT" 1389 | ], 1390 | "authors": [ 1391 | { 1392 | "name": "Nicolas Grekas", 1393 | "email": "p@tchwork.com" 1394 | }, 1395 | { 1396 | "name": "Symfony Community", 1397 | "homepage": "https://symfony.com/contributors" 1398 | } 1399 | ], 1400 | "description": "A generic function and convention to trigger deprecation notices", 1401 | "homepage": "https://symfony.com", 1402 | "support": { 1403 | "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" 1404 | }, 1405 | "funding": [ 1406 | { 1407 | "url": "https://symfony.com/sponsor", 1408 | "type": "custom" 1409 | }, 1410 | { 1411 | "url": "https://github.com/fabpot", 1412 | "type": "github" 1413 | }, 1414 | { 1415 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1416 | "type": "tidelift" 1417 | } 1418 | ], 1419 | "time": "2024-04-18T09:32:20+00:00" 1420 | }, 1421 | { 1422 | "name": "symfony/event-dispatcher", 1423 | "version": "v6.4.13", 1424 | "source": { 1425 | "type": "git", 1426 | "url": "https://github.com/symfony/event-dispatcher.git", 1427 | "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e" 1428 | }, 1429 | "dist": { 1430 | "type": "zip", 1431 | "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", 1432 | "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", 1433 | "shasum": "" 1434 | }, 1435 | "require": { 1436 | "php": ">=8.1", 1437 | "symfony/event-dispatcher-contracts": "^2.5|^3" 1438 | }, 1439 | "conflict": { 1440 | "symfony/dependency-injection": "<5.4", 1441 | "symfony/service-contracts": "<2.5" 1442 | }, 1443 | "provide": { 1444 | "psr/event-dispatcher-implementation": "1.0", 1445 | "symfony/event-dispatcher-implementation": "2.0|3.0" 1446 | }, 1447 | "require-dev": { 1448 | "psr/log": "^1|^2|^3", 1449 | "symfony/config": "^5.4|^6.0|^7.0", 1450 | "symfony/dependency-injection": "^5.4|^6.0|^7.0", 1451 | "symfony/error-handler": "^5.4|^6.0|^7.0", 1452 | "symfony/expression-language": "^5.4|^6.0|^7.0", 1453 | "symfony/http-foundation": "^5.4|^6.0|^7.0", 1454 | "symfony/service-contracts": "^2.5|^3", 1455 | "symfony/stopwatch": "^5.4|^6.0|^7.0" 1456 | }, 1457 | "type": "library", 1458 | "autoload": { 1459 | "psr-4": { 1460 | "Symfony\\Component\\EventDispatcher\\": "" 1461 | }, 1462 | "exclude-from-classmap": [ 1463 | "/Tests/" 1464 | ] 1465 | }, 1466 | "notification-url": "https://packagist.org/downloads/", 1467 | "license": [ 1468 | "MIT" 1469 | ], 1470 | "authors": [ 1471 | { 1472 | "name": "Fabien Potencier", 1473 | "email": "fabien@symfony.com" 1474 | }, 1475 | { 1476 | "name": "Symfony Community", 1477 | "homepage": "https://symfony.com/contributors" 1478 | } 1479 | ], 1480 | "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", 1481 | "homepage": "https://symfony.com", 1482 | "support": { 1483 | "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.13" 1484 | }, 1485 | "funding": [ 1486 | { 1487 | "url": "https://symfony.com/sponsor", 1488 | "type": "custom" 1489 | }, 1490 | { 1491 | "url": "https://github.com/fabpot", 1492 | "type": "github" 1493 | }, 1494 | { 1495 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1496 | "type": "tidelift" 1497 | } 1498 | ], 1499 | "time": "2024-09-25T14:18:03+00:00" 1500 | }, 1501 | { 1502 | "name": "symfony/event-dispatcher-contracts", 1503 | "version": "v3.5.0", 1504 | "source": { 1505 | "type": "git", 1506 | "url": "https://github.com/symfony/event-dispatcher-contracts.git", 1507 | "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" 1508 | }, 1509 | "dist": { 1510 | "type": "zip", 1511 | "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", 1512 | "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", 1513 | "shasum": "" 1514 | }, 1515 | "require": { 1516 | "php": ">=8.1", 1517 | "psr/event-dispatcher": "^1" 1518 | }, 1519 | "type": "library", 1520 | "extra": { 1521 | "branch-alias": { 1522 | "dev-main": "3.5-dev" 1523 | }, 1524 | "thanks": { 1525 | "name": "symfony/contracts", 1526 | "url": "https://github.com/symfony/contracts" 1527 | } 1528 | }, 1529 | "autoload": { 1530 | "psr-4": { 1531 | "Symfony\\Contracts\\EventDispatcher\\": "" 1532 | } 1533 | }, 1534 | "notification-url": "https://packagist.org/downloads/", 1535 | "license": [ 1536 | "MIT" 1537 | ], 1538 | "authors": [ 1539 | { 1540 | "name": "Nicolas Grekas", 1541 | "email": "p@tchwork.com" 1542 | }, 1543 | { 1544 | "name": "Symfony Community", 1545 | "homepage": "https://symfony.com/contributors" 1546 | } 1547 | ], 1548 | "description": "Generic abstractions related to dispatching event", 1549 | "homepage": "https://symfony.com", 1550 | "keywords": [ 1551 | "abstractions", 1552 | "contracts", 1553 | "decoupling", 1554 | "interfaces", 1555 | "interoperability", 1556 | "standards" 1557 | ], 1558 | "support": { 1559 | "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" 1560 | }, 1561 | "funding": [ 1562 | { 1563 | "url": "https://symfony.com/sponsor", 1564 | "type": "custom" 1565 | }, 1566 | { 1567 | "url": "https://github.com/fabpot", 1568 | "type": "github" 1569 | }, 1570 | { 1571 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1572 | "type": "tidelift" 1573 | } 1574 | ], 1575 | "time": "2024-04-18T09:32:20+00:00" 1576 | }, 1577 | { 1578 | "name": "symfony/filesystem", 1579 | "version": "v6.4.13", 1580 | "source": { 1581 | "type": "git", 1582 | "url": "https://github.com/symfony/filesystem.git", 1583 | "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3" 1584 | }, 1585 | "dist": { 1586 | "type": "zip", 1587 | "url": "https://api.github.com/repos/symfony/filesystem/zipball/4856c9cf585d5a0313d8d35afd681a526f038dd3", 1588 | "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3", 1589 | "shasum": "" 1590 | }, 1591 | "require": { 1592 | "php": ">=8.1", 1593 | "symfony/polyfill-ctype": "~1.8", 1594 | "symfony/polyfill-mbstring": "~1.8" 1595 | }, 1596 | "require-dev": { 1597 | "symfony/process": "^5.4|^6.4|^7.0" 1598 | }, 1599 | "type": "library", 1600 | "autoload": { 1601 | "psr-4": { 1602 | "Symfony\\Component\\Filesystem\\": "" 1603 | }, 1604 | "exclude-from-classmap": [ 1605 | "/Tests/" 1606 | ] 1607 | }, 1608 | "notification-url": "https://packagist.org/downloads/", 1609 | "license": [ 1610 | "MIT" 1611 | ], 1612 | "authors": [ 1613 | { 1614 | "name": "Fabien Potencier", 1615 | "email": "fabien@symfony.com" 1616 | }, 1617 | { 1618 | "name": "Symfony Community", 1619 | "homepage": "https://symfony.com/contributors" 1620 | } 1621 | ], 1622 | "description": "Provides basic utilities for the filesystem", 1623 | "homepage": "https://symfony.com", 1624 | "support": { 1625 | "source": "https://github.com/symfony/filesystem/tree/v6.4.13" 1626 | }, 1627 | "funding": [ 1628 | { 1629 | "url": "https://symfony.com/sponsor", 1630 | "type": "custom" 1631 | }, 1632 | { 1633 | "url": "https://github.com/fabpot", 1634 | "type": "github" 1635 | }, 1636 | { 1637 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1638 | "type": "tidelift" 1639 | } 1640 | ], 1641 | "time": "2024-10-25T15:07:50+00:00" 1642 | }, 1643 | { 1644 | "name": "symfony/finder", 1645 | "version": "v6.4.13", 1646 | "source": { 1647 | "type": "git", 1648 | "url": "https://github.com/symfony/finder.git", 1649 | "reference": "daea9eca0b08d0ed1dc9ab702a46128fd1be4958" 1650 | }, 1651 | "dist": { 1652 | "type": "zip", 1653 | "url": "https://api.github.com/repos/symfony/finder/zipball/daea9eca0b08d0ed1dc9ab702a46128fd1be4958", 1654 | "reference": "daea9eca0b08d0ed1dc9ab702a46128fd1be4958", 1655 | "shasum": "" 1656 | }, 1657 | "require": { 1658 | "php": ">=8.1" 1659 | }, 1660 | "require-dev": { 1661 | "symfony/filesystem": "^6.0|^7.0" 1662 | }, 1663 | "type": "library", 1664 | "autoload": { 1665 | "psr-4": { 1666 | "Symfony\\Component\\Finder\\": "" 1667 | }, 1668 | "exclude-from-classmap": [ 1669 | "/Tests/" 1670 | ] 1671 | }, 1672 | "notification-url": "https://packagist.org/downloads/", 1673 | "license": [ 1674 | "MIT" 1675 | ], 1676 | "authors": [ 1677 | { 1678 | "name": "Fabien Potencier", 1679 | "email": "fabien@symfony.com" 1680 | }, 1681 | { 1682 | "name": "Symfony Community", 1683 | "homepage": "https://symfony.com/contributors" 1684 | } 1685 | ], 1686 | "description": "Finds files and directories via an intuitive fluent interface", 1687 | "homepage": "https://symfony.com", 1688 | "support": { 1689 | "source": "https://github.com/symfony/finder/tree/v6.4.13" 1690 | }, 1691 | "funding": [ 1692 | { 1693 | "url": "https://symfony.com/sponsor", 1694 | "type": "custom" 1695 | }, 1696 | { 1697 | "url": "https://github.com/fabpot", 1698 | "type": "github" 1699 | }, 1700 | { 1701 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1702 | "type": "tidelift" 1703 | } 1704 | ], 1705 | "time": "2024-10-01T08:30:56+00:00" 1706 | }, 1707 | { 1708 | "name": "symfony/options-resolver", 1709 | "version": "v6.4.13", 1710 | "source": { 1711 | "type": "git", 1712 | "url": "https://github.com/symfony/options-resolver.git", 1713 | "reference": "0a62a9f2504a8dd27083f89d21894ceb01cc59db" 1714 | }, 1715 | "dist": { 1716 | "type": "zip", 1717 | "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0a62a9f2504a8dd27083f89d21894ceb01cc59db", 1718 | "reference": "0a62a9f2504a8dd27083f89d21894ceb01cc59db", 1719 | "shasum": "" 1720 | }, 1721 | "require": { 1722 | "php": ">=8.1", 1723 | "symfony/deprecation-contracts": "^2.5|^3" 1724 | }, 1725 | "type": "library", 1726 | "autoload": { 1727 | "psr-4": { 1728 | "Symfony\\Component\\OptionsResolver\\": "" 1729 | }, 1730 | "exclude-from-classmap": [ 1731 | "/Tests/" 1732 | ] 1733 | }, 1734 | "notification-url": "https://packagist.org/downloads/", 1735 | "license": [ 1736 | "MIT" 1737 | ], 1738 | "authors": [ 1739 | { 1740 | "name": "Fabien Potencier", 1741 | "email": "fabien@symfony.com" 1742 | }, 1743 | { 1744 | "name": "Symfony Community", 1745 | "homepage": "https://symfony.com/contributors" 1746 | } 1747 | ], 1748 | "description": "Provides an improved replacement for the array_replace PHP function", 1749 | "homepage": "https://symfony.com", 1750 | "keywords": [ 1751 | "config", 1752 | "configuration", 1753 | "options" 1754 | ], 1755 | "support": { 1756 | "source": "https://github.com/symfony/options-resolver/tree/v6.4.13" 1757 | }, 1758 | "funding": [ 1759 | { 1760 | "url": "https://symfony.com/sponsor", 1761 | "type": "custom" 1762 | }, 1763 | { 1764 | "url": "https://github.com/fabpot", 1765 | "type": "github" 1766 | }, 1767 | { 1768 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1769 | "type": "tidelift" 1770 | } 1771 | ], 1772 | "time": "2024-09-25T14:18:03+00:00" 1773 | }, 1774 | { 1775 | "name": "symfony/polyfill-ctype", 1776 | "version": "v1.31.0", 1777 | "source": { 1778 | "type": "git", 1779 | "url": "https://github.com/symfony/polyfill-ctype.git", 1780 | "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" 1781 | }, 1782 | "dist": { 1783 | "type": "zip", 1784 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", 1785 | "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", 1786 | "shasum": "" 1787 | }, 1788 | "require": { 1789 | "php": ">=7.2" 1790 | }, 1791 | "provide": { 1792 | "ext-ctype": "*" 1793 | }, 1794 | "suggest": { 1795 | "ext-ctype": "For best performance" 1796 | }, 1797 | "type": "library", 1798 | "extra": { 1799 | "thanks": { 1800 | "name": "symfony/polyfill", 1801 | "url": "https://github.com/symfony/polyfill" 1802 | } 1803 | }, 1804 | "autoload": { 1805 | "files": [ 1806 | "bootstrap.php" 1807 | ], 1808 | "psr-4": { 1809 | "Symfony\\Polyfill\\Ctype\\": "" 1810 | } 1811 | }, 1812 | "notification-url": "https://packagist.org/downloads/", 1813 | "license": [ 1814 | "MIT" 1815 | ], 1816 | "authors": [ 1817 | { 1818 | "name": "Gert de Pagter", 1819 | "email": "BackEndTea@gmail.com" 1820 | }, 1821 | { 1822 | "name": "Symfony Community", 1823 | "homepage": "https://symfony.com/contributors" 1824 | } 1825 | ], 1826 | "description": "Symfony polyfill for ctype functions", 1827 | "homepage": "https://symfony.com", 1828 | "keywords": [ 1829 | "compatibility", 1830 | "ctype", 1831 | "polyfill", 1832 | "portable" 1833 | ], 1834 | "support": { 1835 | "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" 1836 | }, 1837 | "funding": [ 1838 | { 1839 | "url": "https://symfony.com/sponsor", 1840 | "type": "custom" 1841 | }, 1842 | { 1843 | "url": "https://github.com/fabpot", 1844 | "type": "github" 1845 | }, 1846 | { 1847 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1848 | "type": "tidelift" 1849 | } 1850 | ], 1851 | "time": "2024-09-09T11:45:10+00:00" 1852 | }, 1853 | { 1854 | "name": "symfony/polyfill-intl-grapheme", 1855 | "version": "v1.31.0", 1856 | "source": { 1857 | "type": "git", 1858 | "url": "https://github.com/symfony/polyfill-intl-grapheme.git", 1859 | "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" 1860 | }, 1861 | "dist": { 1862 | "type": "zip", 1863 | "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", 1864 | "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", 1865 | "shasum": "" 1866 | }, 1867 | "require": { 1868 | "php": ">=7.2" 1869 | }, 1870 | "suggest": { 1871 | "ext-intl": "For best performance" 1872 | }, 1873 | "type": "library", 1874 | "extra": { 1875 | "thanks": { 1876 | "name": "symfony/polyfill", 1877 | "url": "https://github.com/symfony/polyfill" 1878 | } 1879 | }, 1880 | "autoload": { 1881 | "files": [ 1882 | "bootstrap.php" 1883 | ], 1884 | "psr-4": { 1885 | "Symfony\\Polyfill\\Intl\\Grapheme\\": "" 1886 | } 1887 | }, 1888 | "notification-url": "https://packagist.org/downloads/", 1889 | "license": [ 1890 | "MIT" 1891 | ], 1892 | "authors": [ 1893 | { 1894 | "name": "Nicolas Grekas", 1895 | "email": "p@tchwork.com" 1896 | }, 1897 | { 1898 | "name": "Symfony Community", 1899 | "homepage": "https://symfony.com/contributors" 1900 | } 1901 | ], 1902 | "description": "Symfony polyfill for intl's grapheme_* functions", 1903 | "homepage": "https://symfony.com", 1904 | "keywords": [ 1905 | "compatibility", 1906 | "grapheme", 1907 | "intl", 1908 | "polyfill", 1909 | "portable", 1910 | "shim" 1911 | ], 1912 | "support": { 1913 | "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" 1914 | }, 1915 | "funding": [ 1916 | { 1917 | "url": "https://symfony.com/sponsor", 1918 | "type": "custom" 1919 | }, 1920 | { 1921 | "url": "https://github.com/fabpot", 1922 | "type": "github" 1923 | }, 1924 | { 1925 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1926 | "type": "tidelift" 1927 | } 1928 | ], 1929 | "time": "2024-09-09T11:45:10+00:00" 1930 | }, 1931 | { 1932 | "name": "symfony/polyfill-intl-normalizer", 1933 | "version": "v1.31.0", 1934 | "source": { 1935 | "type": "git", 1936 | "url": "https://github.com/symfony/polyfill-intl-normalizer.git", 1937 | "reference": "3833d7255cc303546435cb650316bff708a1c75c" 1938 | }, 1939 | "dist": { 1940 | "type": "zip", 1941 | "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", 1942 | "reference": "3833d7255cc303546435cb650316bff708a1c75c", 1943 | "shasum": "" 1944 | }, 1945 | "require": { 1946 | "php": ">=7.2" 1947 | }, 1948 | "suggest": { 1949 | "ext-intl": "For best performance" 1950 | }, 1951 | "type": "library", 1952 | "extra": { 1953 | "thanks": { 1954 | "name": "symfony/polyfill", 1955 | "url": "https://github.com/symfony/polyfill" 1956 | } 1957 | }, 1958 | "autoload": { 1959 | "files": [ 1960 | "bootstrap.php" 1961 | ], 1962 | "psr-4": { 1963 | "Symfony\\Polyfill\\Intl\\Normalizer\\": "" 1964 | }, 1965 | "classmap": [ 1966 | "Resources/stubs" 1967 | ] 1968 | }, 1969 | "notification-url": "https://packagist.org/downloads/", 1970 | "license": [ 1971 | "MIT" 1972 | ], 1973 | "authors": [ 1974 | { 1975 | "name": "Nicolas Grekas", 1976 | "email": "p@tchwork.com" 1977 | }, 1978 | { 1979 | "name": "Symfony Community", 1980 | "homepage": "https://symfony.com/contributors" 1981 | } 1982 | ], 1983 | "description": "Symfony polyfill for intl's Normalizer class and related functions", 1984 | "homepage": "https://symfony.com", 1985 | "keywords": [ 1986 | "compatibility", 1987 | "intl", 1988 | "normalizer", 1989 | "polyfill", 1990 | "portable", 1991 | "shim" 1992 | ], 1993 | "support": { 1994 | "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" 1995 | }, 1996 | "funding": [ 1997 | { 1998 | "url": "https://symfony.com/sponsor", 1999 | "type": "custom" 2000 | }, 2001 | { 2002 | "url": "https://github.com/fabpot", 2003 | "type": "github" 2004 | }, 2005 | { 2006 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2007 | "type": "tidelift" 2008 | } 2009 | ], 2010 | "time": "2024-09-09T11:45:10+00:00" 2011 | }, 2012 | { 2013 | "name": "symfony/polyfill-mbstring", 2014 | "version": "v1.31.0", 2015 | "source": { 2016 | "type": "git", 2017 | "url": "https://github.com/symfony/polyfill-mbstring.git", 2018 | "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" 2019 | }, 2020 | "dist": { 2021 | "type": "zip", 2022 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", 2023 | "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", 2024 | "shasum": "" 2025 | }, 2026 | "require": { 2027 | "php": ">=7.2" 2028 | }, 2029 | "provide": { 2030 | "ext-mbstring": "*" 2031 | }, 2032 | "suggest": { 2033 | "ext-mbstring": "For best performance" 2034 | }, 2035 | "type": "library", 2036 | "extra": { 2037 | "thanks": { 2038 | "name": "symfony/polyfill", 2039 | "url": "https://github.com/symfony/polyfill" 2040 | } 2041 | }, 2042 | "autoload": { 2043 | "files": [ 2044 | "bootstrap.php" 2045 | ], 2046 | "psr-4": { 2047 | "Symfony\\Polyfill\\Mbstring\\": "" 2048 | } 2049 | }, 2050 | "notification-url": "https://packagist.org/downloads/", 2051 | "license": [ 2052 | "MIT" 2053 | ], 2054 | "authors": [ 2055 | { 2056 | "name": "Nicolas Grekas", 2057 | "email": "p@tchwork.com" 2058 | }, 2059 | { 2060 | "name": "Symfony Community", 2061 | "homepage": "https://symfony.com/contributors" 2062 | } 2063 | ], 2064 | "description": "Symfony polyfill for the Mbstring extension", 2065 | "homepage": "https://symfony.com", 2066 | "keywords": [ 2067 | "compatibility", 2068 | "mbstring", 2069 | "polyfill", 2070 | "portable", 2071 | "shim" 2072 | ], 2073 | "support": { 2074 | "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" 2075 | }, 2076 | "funding": [ 2077 | { 2078 | "url": "https://symfony.com/sponsor", 2079 | "type": "custom" 2080 | }, 2081 | { 2082 | "url": "https://github.com/fabpot", 2083 | "type": "github" 2084 | }, 2085 | { 2086 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2087 | "type": "tidelift" 2088 | } 2089 | ], 2090 | "time": "2024-09-09T11:45:10+00:00" 2091 | }, 2092 | { 2093 | "name": "symfony/polyfill-php80", 2094 | "version": "v1.31.0", 2095 | "source": { 2096 | "type": "git", 2097 | "url": "https://github.com/symfony/polyfill-php80.git", 2098 | "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" 2099 | }, 2100 | "dist": { 2101 | "type": "zip", 2102 | "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", 2103 | "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", 2104 | "shasum": "" 2105 | }, 2106 | "require": { 2107 | "php": ">=7.2" 2108 | }, 2109 | "type": "library", 2110 | "extra": { 2111 | "thanks": { 2112 | "name": "symfony/polyfill", 2113 | "url": "https://github.com/symfony/polyfill" 2114 | } 2115 | }, 2116 | "autoload": { 2117 | "files": [ 2118 | "bootstrap.php" 2119 | ], 2120 | "psr-4": { 2121 | "Symfony\\Polyfill\\Php80\\": "" 2122 | }, 2123 | "classmap": [ 2124 | "Resources/stubs" 2125 | ] 2126 | }, 2127 | "notification-url": "https://packagist.org/downloads/", 2128 | "license": [ 2129 | "MIT" 2130 | ], 2131 | "authors": [ 2132 | { 2133 | "name": "Ion Bazan", 2134 | "email": "ion.bazan@gmail.com" 2135 | }, 2136 | { 2137 | "name": "Nicolas Grekas", 2138 | "email": "p@tchwork.com" 2139 | }, 2140 | { 2141 | "name": "Symfony Community", 2142 | "homepage": "https://symfony.com/contributors" 2143 | } 2144 | ], 2145 | "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", 2146 | "homepage": "https://symfony.com", 2147 | "keywords": [ 2148 | "compatibility", 2149 | "polyfill", 2150 | "portable", 2151 | "shim" 2152 | ], 2153 | "support": { 2154 | "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" 2155 | }, 2156 | "funding": [ 2157 | { 2158 | "url": "https://symfony.com/sponsor", 2159 | "type": "custom" 2160 | }, 2161 | { 2162 | "url": "https://github.com/fabpot", 2163 | "type": "github" 2164 | }, 2165 | { 2166 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2167 | "type": "tidelift" 2168 | } 2169 | ], 2170 | "time": "2024-09-09T11:45:10+00:00" 2171 | }, 2172 | { 2173 | "name": "symfony/polyfill-php81", 2174 | "version": "v1.31.0", 2175 | "source": { 2176 | "type": "git", 2177 | "url": "https://github.com/symfony/polyfill-php81.git", 2178 | "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" 2179 | }, 2180 | "dist": { 2181 | "type": "zip", 2182 | "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", 2183 | "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", 2184 | "shasum": "" 2185 | }, 2186 | "require": { 2187 | "php": ">=7.2" 2188 | }, 2189 | "type": "library", 2190 | "extra": { 2191 | "thanks": { 2192 | "name": "symfony/polyfill", 2193 | "url": "https://github.com/symfony/polyfill" 2194 | } 2195 | }, 2196 | "autoload": { 2197 | "files": [ 2198 | "bootstrap.php" 2199 | ], 2200 | "psr-4": { 2201 | "Symfony\\Polyfill\\Php81\\": "" 2202 | }, 2203 | "classmap": [ 2204 | "Resources/stubs" 2205 | ] 2206 | }, 2207 | "notification-url": "https://packagist.org/downloads/", 2208 | "license": [ 2209 | "MIT" 2210 | ], 2211 | "authors": [ 2212 | { 2213 | "name": "Nicolas Grekas", 2214 | "email": "p@tchwork.com" 2215 | }, 2216 | { 2217 | "name": "Symfony Community", 2218 | "homepage": "https://symfony.com/contributors" 2219 | } 2220 | ], 2221 | "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", 2222 | "homepage": "https://symfony.com", 2223 | "keywords": [ 2224 | "compatibility", 2225 | "polyfill", 2226 | "portable", 2227 | "shim" 2228 | ], 2229 | "support": { 2230 | "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" 2231 | }, 2232 | "funding": [ 2233 | { 2234 | "url": "https://symfony.com/sponsor", 2235 | "type": "custom" 2236 | }, 2237 | { 2238 | "url": "https://github.com/fabpot", 2239 | "type": "github" 2240 | }, 2241 | { 2242 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2243 | "type": "tidelift" 2244 | } 2245 | ], 2246 | "time": "2024-09-09T11:45:10+00:00" 2247 | }, 2248 | { 2249 | "name": "symfony/process", 2250 | "version": "v6.4.13", 2251 | "source": { 2252 | "type": "git", 2253 | "url": "https://github.com/symfony/process.git", 2254 | "reference": "1f9f59b46880201629df3bd950fc5ae8c55b960f" 2255 | }, 2256 | "dist": { 2257 | "type": "zip", 2258 | "url": "https://api.github.com/repos/symfony/process/zipball/1f9f59b46880201629df3bd950fc5ae8c55b960f", 2259 | "reference": "1f9f59b46880201629df3bd950fc5ae8c55b960f", 2260 | "shasum": "" 2261 | }, 2262 | "require": { 2263 | "php": ">=8.1" 2264 | }, 2265 | "type": "library", 2266 | "autoload": { 2267 | "psr-4": { 2268 | "Symfony\\Component\\Process\\": "" 2269 | }, 2270 | "exclude-from-classmap": [ 2271 | "/Tests/" 2272 | ] 2273 | }, 2274 | "notification-url": "https://packagist.org/downloads/", 2275 | "license": [ 2276 | "MIT" 2277 | ], 2278 | "authors": [ 2279 | { 2280 | "name": "Fabien Potencier", 2281 | "email": "fabien@symfony.com" 2282 | }, 2283 | { 2284 | "name": "Symfony Community", 2285 | "homepage": "https://symfony.com/contributors" 2286 | } 2287 | ], 2288 | "description": "Executes commands in sub-processes", 2289 | "homepage": "https://symfony.com", 2290 | "support": { 2291 | "source": "https://github.com/symfony/process/tree/v6.4.13" 2292 | }, 2293 | "funding": [ 2294 | { 2295 | "url": "https://symfony.com/sponsor", 2296 | "type": "custom" 2297 | }, 2298 | { 2299 | "url": "https://github.com/fabpot", 2300 | "type": "github" 2301 | }, 2302 | { 2303 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2304 | "type": "tidelift" 2305 | } 2306 | ], 2307 | "time": "2024-09-25T14:18:03+00:00" 2308 | }, 2309 | { 2310 | "name": "symfony/service-contracts", 2311 | "version": "v3.5.0", 2312 | "source": { 2313 | "type": "git", 2314 | "url": "https://github.com/symfony/service-contracts.git", 2315 | "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" 2316 | }, 2317 | "dist": { 2318 | "type": "zip", 2319 | "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", 2320 | "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", 2321 | "shasum": "" 2322 | }, 2323 | "require": { 2324 | "php": ">=8.1", 2325 | "psr/container": "^1.1|^2.0", 2326 | "symfony/deprecation-contracts": "^2.5|^3" 2327 | }, 2328 | "conflict": { 2329 | "ext-psr": "<1.1|>=2" 2330 | }, 2331 | "type": "library", 2332 | "extra": { 2333 | "branch-alias": { 2334 | "dev-main": "3.5-dev" 2335 | }, 2336 | "thanks": { 2337 | "name": "symfony/contracts", 2338 | "url": "https://github.com/symfony/contracts" 2339 | } 2340 | }, 2341 | "autoload": { 2342 | "psr-4": { 2343 | "Symfony\\Contracts\\Service\\": "" 2344 | }, 2345 | "exclude-from-classmap": [ 2346 | "/Test/" 2347 | ] 2348 | }, 2349 | "notification-url": "https://packagist.org/downloads/", 2350 | "license": [ 2351 | "MIT" 2352 | ], 2353 | "authors": [ 2354 | { 2355 | "name": "Nicolas Grekas", 2356 | "email": "p@tchwork.com" 2357 | }, 2358 | { 2359 | "name": "Symfony Community", 2360 | "homepage": "https://symfony.com/contributors" 2361 | } 2362 | ], 2363 | "description": "Generic abstractions related to writing services", 2364 | "homepage": "https://symfony.com", 2365 | "keywords": [ 2366 | "abstractions", 2367 | "contracts", 2368 | "decoupling", 2369 | "interfaces", 2370 | "interoperability", 2371 | "standards" 2372 | ], 2373 | "support": { 2374 | "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" 2375 | }, 2376 | "funding": [ 2377 | { 2378 | "url": "https://symfony.com/sponsor", 2379 | "type": "custom" 2380 | }, 2381 | { 2382 | "url": "https://github.com/fabpot", 2383 | "type": "github" 2384 | }, 2385 | { 2386 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2387 | "type": "tidelift" 2388 | } 2389 | ], 2390 | "time": "2024-04-18T09:32:20+00:00" 2391 | }, 2392 | { 2393 | "name": "symfony/stopwatch", 2394 | "version": "v6.4.13", 2395 | "source": { 2396 | "type": "git", 2397 | "url": "https://github.com/symfony/stopwatch.git", 2398 | "reference": "2cae0a6f8d04937d02f6d19806251e2104d54f92" 2399 | }, 2400 | "dist": { 2401 | "type": "zip", 2402 | "url": "https://api.github.com/repos/symfony/stopwatch/zipball/2cae0a6f8d04937d02f6d19806251e2104d54f92", 2403 | "reference": "2cae0a6f8d04937d02f6d19806251e2104d54f92", 2404 | "shasum": "" 2405 | }, 2406 | "require": { 2407 | "php": ">=8.1", 2408 | "symfony/service-contracts": "^2.5|^3" 2409 | }, 2410 | "type": "library", 2411 | "autoload": { 2412 | "psr-4": { 2413 | "Symfony\\Component\\Stopwatch\\": "" 2414 | }, 2415 | "exclude-from-classmap": [ 2416 | "/Tests/" 2417 | ] 2418 | }, 2419 | "notification-url": "https://packagist.org/downloads/", 2420 | "license": [ 2421 | "MIT" 2422 | ], 2423 | "authors": [ 2424 | { 2425 | "name": "Fabien Potencier", 2426 | "email": "fabien@symfony.com" 2427 | }, 2428 | { 2429 | "name": "Symfony Community", 2430 | "homepage": "https://symfony.com/contributors" 2431 | } 2432 | ], 2433 | "description": "Provides a way to profile code", 2434 | "homepage": "https://symfony.com", 2435 | "support": { 2436 | "source": "https://github.com/symfony/stopwatch/tree/v6.4.13" 2437 | }, 2438 | "funding": [ 2439 | { 2440 | "url": "https://symfony.com/sponsor", 2441 | "type": "custom" 2442 | }, 2443 | { 2444 | "url": "https://github.com/fabpot", 2445 | "type": "github" 2446 | }, 2447 | { 2448 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2449 | "type": "tidelift" 2450 | } 2451 | ], 2452 | "time": "2024-09-25T14:18:03+00:00" 2453 | }, 2454 | { 2455 | "name": "symfony/string", 2456 | "version": "v6.4.13", 2457 | "source": { 2458 | "type": "git", 2459 | "url": "https://github.com/symfony/string.git", 2460 | "reference": "38371c60c71c72b3d64d8d76f6b1bb81a2cc3627" 2461 | }, 2462 | "dist": { 2463 | "type": "zip", 2464 | "url": "https://api.github.com/repos/symfony/string/zipball/38371c60c71c72b3d64d8d76f6b1bb81a2cc3627", 2465 | "reference": "38371c60c71c72b3d64d8d76f6b1bb81a2cc3627", 2466 | "shasum": "" 2467 | }, 2468 | "require": { 2469 | "php": ">=8.1", 2470 | "symfony/polyfill-ctype": "~1.8", 2471 | "symfony/polyfill-intl-grapheme": "~1.0", 2472 | "symfony/polyfill-intl-normalizer": "~1.0", 2473 | "symfony/polyfill-mbstring": "~1.0" 2474 | }, 2475 | "conflict": { 2476 | "symfony/translation-contracts": "<2.5" 2477 | }, 2478 | "require-dev": { 2479 | "symfony/error-handler": "^5.4|^6.0|^7.0", 2480 | "symfony/http-client": "^5.4|^6.0|^7.0", 2481 | "symfony/intl": "^6.2|^7.0", 2482 | "symfony/translation-contracts": "^2.5|^3.0", 2483 | "symfony/var-exporter": "^5.4|^6.0|^7.0" 2484 | }, 2485 | "type": "library", 2486 | "autoload": { 2487 | "files": [ 2488 | "Resources/functions.php" 2489 | ], 2490 | "psr-4": { 2491 | "Symfony\\Component\\String\\": "" 2492 | }, 2493 | "exclude-from-classmap": [ 2494 | "/Tests/" 2495 | ] 2496 | }, 2497 | "notification-url": "https://packagist.org/downloads/", 2498 | "license": [ 2499 | "MIT" 2500 | ], 2501 | "authors": [ 2502 | { 2503 | "name": "Nicolas Grekas", 2504 | "email": "p@tchwork.com" 2505 | }, 2506 | { 2507 | "name": "Symfony Community", 2508 | "homepage": "https://symfony.com/contributors" 2509 | } 2510 | ], 2511 | "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", 2512 | "homepage": "https://symfony.com", 2513 | "keywords": [ 2514 | "grapheme", 2515 | "i18n", 2516 | "string", 2517 | "unicode", 2518 | "utf-8", 2519 | "utf8" 2520 | ], 2521 | "support": { 2522 | "source": "https://github.com/symfony/string/tree/v6.4.13" 2523 | }, 2524 | "funding": [ 2525 | { 2526 | "url": "https://symfony.com/sponsor", 2527 | "type": "custom" 2528 | }, 2529 | { 2530 | "url": "https://github.com/fabpot", 2531 | "type": "github" 2532 | }, 2533 | { 2534 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2535 | "type": "tidelift" 2536 | } 2537 | ], 2538 | "time": "2024-09-25T14:18:03+00:00" 2539 | } 2540 | ], 2541 | "packages-dev": [], 2542 | "aliases": [], 2543 | "minimum-stability": "stable", 2544 | "stability-flags": {}, 2545 | "prefer-stable": false, 2546 | "prefer-lowest": false, 2547 | "platform": {}, 2548 | "platform-dev": {}, 2549 | "platform-overrides": { 2550 | "php": "8.1" 2551 | }, 2552 | "plugin-api-version": "2.6.0" 2553 | } 2554 | --------------------------------------------------------------------------------