├── 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 |
--------------------------------------------------------------------------------