diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9cdac0b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +**/*.log +**/*.md +**/*.php~ +**/*.dist.php +**/*.dist +**/*.cache +**/._* +**/.dockerignore +**/.DS_Store +**/.git/ +**/.gitattributes +**/.gitignore +**/.gitmodules +**/docker-compose.*.yaml +**/docker-compose.*.yml +**/docker-compose.yaml +**/docker-compose.yml +**/Dockerfile +**/Thumbs.db +.github/ +docs/ +helm/ +Makefile +node_modules/ +public/build/ +public/bundles/ +tests/ +var/ +vendor/ +.editorconfig +.env.*.local +.env.local +.env.local.php +.env.test diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3ac9219 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,72 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] +# Change these settings to your own preference +indent_style = space +indent_size = 4 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{js,html,ts,tsx}] +indent_style = space +indent_size = 2 + +[*.json] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.php] +indent_style = space +indent_size = 4 + +[*.sh] +indent_style = tab +indent_size = 4 + +[*.xml{,.dist}] +indent_style = space +indent_size = 4 + +[*.{yaml,yml}] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = false + +[.github/workflows/*.yml] +indent_style = space +indent_size = 2 + +[.gitmodules] +indent_style = tab +indent_size = 4 + +[.php_cs{,.dist}] +indent_style = space +indent_size = 4 + +[.travis.yml] +indent_style = space +indent_size = 2 + +[composer.json] +indent_style = space +indent_size = 4 + +[docker-compose{,.*}.{yaml,yml}] +indent_style = space +indent_size = 2 + +[Dockerfile] +indent_style = tab +indent_size = 4 diff --git a/.env b/.env new file mode 100644 index 0000000..abc5c9b --- /dev/null +++ b/.env @@ -0,0 +1,108 @@ +# In all environments, the following files are loaded if they exist, +# the latter taking precedence over the former: +# +# * .env contains default values for the environment variables needed by the app +# * .env.local uncommitted file with local overrides +# * .env.$APP_ENV committed environment-specific defaults +# * .env.$APP_ENV.local uncommitted environment-specific overrides +# +# Real environment variables win over .env files. +# +# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# https://symfony.com/doc/current/configuration/secrets.html +# +# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). +# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration + +###> symfony/framework-bundle ### +APP_ENV=dev +APP_DEBUG=1 +APP_SECRET=fd8e6e32f64b1dddcfe849f88de35019 +###< symfony/framework-bundle ### + +###> doctrine/doctrine-bundle ### +# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml +# +# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" +# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8&charset=utf8mb4" +DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=14&charset=utf8" +###< doctrine/doctrine-bundle ### + +###> nelmio/cors-bundle ### +CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' +###< nelmio/cors-bundle ### + +###> symfony/messenger ### +# Choose one of the transports below +MESSENGER_TRANSPORT_DSN=doctrine://default +# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages +# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages +###< symfony/messenger ### + +###> symfony/mercure-bundle ### +# See https://symfony.com/doc/current/mercure.html#configuration +# The URL of the Mercure hub, used by the app to publish updates (can be a local URL) +MERCURE_URL=https://example.com/.well-known/mercure +# The public URL of the Mercure hub, used by the browser to connect +MERCURE_PUBLIC_URL=https://example.com/.well-known/mercure +# The secret used to sign the JWTs +MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!" +###< symfony/mercure-bundle ### + +###> symfony/mailer ### +MAILER_DSN=null://null +# MAILER_DSN=smtp://mailer:1025 +# MAILER_DSN=null://null +###< symfony/mailer ### + +###> symfony/google-mailer ### +# Gmail SHOULD NOT be used on production, use it in development only. +# It can be used with an application password, see https://support.google.com/accounts/answer/185833?visit_id=638042796580302159-1820620508&p=InvalidSecondFactor&rd=1 +# MAILER_DSN=gmail://USERNAME:PASSWORD@default +###< symfony/google-mailer ### + +###> symfony/twilio-notifier ### +# SMS_DSN=twilio://SID:TOKEN@default?from=FROM +###< symfony/twilio-notifier ### + +###> symfony/ovh-cloud-notifier ### +# SMS_DSN=ovhcloud://APPLICATION_KEY:APPLICATION_SECRET@default?consumer_key=CONSUMER_KEY&service_name=SERVICE_NAME +###< symfony/ovh-cloud-notifier ### + +###> symfony/notifier ### +# This is main the DNS that is used by the notifier component +# the other below are just example for two different third party vendors +#SMS_DSN=twilio://SID:TOKEN@default?from=FROM +SMS_DSN=null://null +###< symfony/notifier ### + +###> symfony/fake-sms-notifier ### +FAKE_SMS_DSN=fakesms+email://mailer?to=admin&from=PlateformCoop +###< symfony/fake-sms-notifier ### + +###> snc/redis-bundle ### +# passwords that contain special characters (@, %, :, +) must be urlencoded +REDIS_URL=redis://redis +###< snc/redis-bundle ### + +###> meilisearch/meilisearch-symfony ### +MEILISEARCH_URL=http://meilisearch:7700 +MEILISEARCH_API_KEY=ms +###< meilisearch/meilisearch-symfony ### + +###> payum/payum-bundle ### +# @see https://my.mollie.com/dashboard/org_XXXXXXXX/developers/api-keys +# even it's a fake key it must start with 'test_' or 'live_' and must be at least 30 characters long +PAYUM_APIKEY=test_FRabcdefghijklmnopqrstuvwxyzab +PAYUM_GATEWAY=mollie +###< payum/payum-bundle ### + +###> league/flysystem-bundle ### +STORAGE_BUCKET=images +STORAGE_ENDPOINT=http://storage:9000 +STORAGE_REGION=us-east-1 +STORAGE_USE_PATH_STYLE_ENDPOINT=true +STORAGE_KEY=app +STORAGE_SECRET=!ChangeMe! +###< league/flysystem-bundle ### diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..1ac4142 --- /dev/null +++ b/.env.test @@ -0,0 +1,7 @@ +# define your env variables for the test env here +KERNEL_CLASS='App\Kernel' +APP_SECRET='$ecretf0rt3st' +SYMFONY_DEPRECATIONS_HELPER=999999 +PANTHER_APP_ENV=test +PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots +PAYUM_GATEWAY=offline diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..4100f74 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": "eslint:recommended", + "overrides": [ + ], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { + "indent": [ + "error", + 2, + { "ignoreComments": true } + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "never" + ] + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..590d686 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,16 @@ +* text=auto eol=lf + +*.conf text eol=lf +*.html text eol=lf +*.ini text eol=lf +*.js text eol=lf +*.json text eol=lf +*.md text eol=lf +*.php text eol=lf +*.sh text eol=lf +*.yaml text eol=lf +*.yml text eol=lf +bin/console text eol=lf + +*.ico binary +*.png binary diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..d6a0af1 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,45 @@ +# Description + +Please include a summary of the changes and the related issue. +Please also include relevant motivation and context. + +Related to # (issue) + +## Type of change + +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Cleanup/refactoring of an existing feature (code quality change without feature modification) +- [ ] Improvment of an existing feature (minor tweak) +- [ ] Documentation (no code) + +# How Has This Been Tested? + +Please describe the tests that you ran to verify your changes. Put below at least a file +that was modified/added for the tests of this feature/bugfix. + +- [ ] Test A + +# Checklist: + +- [ ] I have assigned myself to the related ticket +- [ ] I have read the ticket carefully and the code addresses exactly what is described in it +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] Tests pass locally (`make test-complete`) +- [ ] Code coverage is still at 100% (`make coverage`) +- [ ] My code follows the style guidelines of this project, `make ci` should run without error +- [ ] Translations have been added, I didn't put raw text in PHP files or Twig templates +- [ ] I have performed a self-review of my code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] Git rebase have been made, and the PR can be merged without conflict +- [ ] All CI checks are ✅ +- [ ] I have removed the draft status of the PR +- [ ] I have removed the WIP tag on the PR and also in the title +- [ ] I have added the RFR (ready for review) tag on the PR +- [ ] I have added the DEPLOY tag and the feature/fix can be tested on the dedicated environment +- [ ] I have assigned someone to review the PR + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..49c0df0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI + +on: + push: + +jobs: + build: + name: Docker build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Pull images + run: docker compose pull + - name: Start services + run: docker compose up --build -d + - name: Wait for services + run: | + while status="$(docker inspect --format="{{if .Config.Healthcheck}}{{print .State.Health.Status}}{{end}}" "$(docker compose ps -q php)")"; do + case $status in + starting) sleep 1;; + healthy) exit 0;; + unhealthy) exit 1;; + esac + done + exit 1 + - name: Check HTTP reachability + run: curl http://localhost + - name: Check HTTPS reachability + run: curl -k https://localhost + - name: Test manifest file + run: mkdir -p public/build; docker compose exec -T php echo "{}" > public/build/manifest.json + - name: Create test database + run: docker compose exec -T php bin/console -e test doctrine:database:create + - name: Create database schema + run: docker compose exec -T php bin/console -e test doctrine:schema:create + - name: Setup Messenger + run: docker compose exec -T php bin/console -e test messenger:setup-transports + - name: Initializing Fixtures + run: docker compose exec -T php bin/console -e test hautelook:fixtures:load --no-interaction --no-bundles + - name: Meilisearch indexation + run: docker compose exec -T php bin/console -e test app:index-products + - name: PHPUnit + run: docker compose exec -T php bin/phpunit + - name: Doctrine Schema Validator + run: docker compose exec -T php bin/console doctrine:schema:validate --skip-sync + - name: PHP CS Fixer + run: docker compose exec -T php sh -c './vendor/bin/php-cs-fixer fix --allow-risky=yes --dry-run --format=checkstyle | ./vendor/bin/cs2pr' + - name: PHPStan + run: docker compose exec -T php ./vendor/bin/phpstan analyse --memory-limit=-1 --error-format=github + - name: Twig Linter + run: docker compose exec -T php ./vendor/bin/twigcs templates/ --exclude vendor + - name: Install eslint + run: docker run --rm -w "/usr/app" -v "${PWD}":/usr/app gmolaire/yarn yarn add eslint + - name: Run eslint on javascript files + run: docker run --rm -w "/usr/app" -v "${PWD}":/usr/app gmolaire/yarn yarn lint diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml new file mode 100644 index 0000000..7488082 --- /dev/null +++ b/.github/workflows/cleanup.yml @@ -0,0 +1,33 @@ +name: Cleanup + +on: + pull_request: + types: [ closed ] + +jobs: + cleanup: + name: Cleanup + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set KUBECONFIG + run: | + mkdir ~/.kube + echo ${{ secrets.KUBECONFIG }} | base64 -d > ~/.kube/config + kubectl config view + + - name: Uninstall helm release + id: uninstall_helm_release + run: | + export RELEASE_NAME=pr-$(jq --raw-output .pull_request.number $GITHUB_EVENT_PATH) + echo "Uninstalling release ${RELEASE_NAME}" + if ! helm uninstall ${RELEASE_NAME} --kube-context nonprod --wait ; then + echo "HELM Uninstall has failed !" + echo "Please ask the SRE team to manually clean remaining objects" + exit 1 + fi + echo "HELM uninstall successfull" + echo "Cleaning remaining PVC..." + kubectl delete pvc -l app.kubernetes.io/instance=$RELEASE_NAME diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ea8b40 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ + +###> symfony/framework-bundle ### +/.env.local +/.env.local.php +/.env.*.local +/config/secrets/prod/prod.decrypt.private.php +/public/bundles/ +/var/ +/vendor/ +###< symfony/framework-bundle ### + +###> friendsofphp/php-cs-fixer ### +/.php-cs-fixer.php +/.php-cs-fixer.cache +###< friendsofphp/php-cs-fixer ### + +###> symfony/phpunit-bridge ### +###< symfony/phpunit-bridge ### + +###> phpunit/phpunit ### +/phpunit.xml +.phpunit.result.cache +docs/coverage +###< phpunit/phpunit ### + +###> symfony/webpack-encore-bundle ### +/node_modules/ +/public/build/ +npm-debug.log +yarn-error.log +###< symfony/webpack-encore-bundle ### + +###> league/flysystem-bundle ### +storage +###< league/flysystem-bundle ### diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..c68ff2a --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,27 @@ +in(__DIR__) + ->exclude('var') + ->exclude('tmp') + ->exclude('node_modules') +; + +return (new PhpCsFixer\Config())->setRules([ + '@Symfony' => true, + 'array_syntax' => ['syntax' => 'short'], // https://cs.symfony.com/doc/rules/array_notation/array_syntax.html + 'declare_strict_types' => true, // https://cs.symfony.com/doc/rules/strict/declare_strict_types.html + 'php_unit_fqcn_annotation' => false, // https://cs.symfony.com/doc/rules/php_unit/php_unit_fqcn_annotation.html + 'yoda_style' => false, // https://cs.symfony.com/doc/rules/control_structure/yoda_style.html + 'phpdoc_to_comment' => false, // https://cs.symfony.com/doc/rules/phpdoc/phpdoc_to_comment.html # Needed for PHPStan @var annotations + 'native_function_invocation' => [ // https://cs.symfony.com/doc/rules/function_notation/native_function_invocation.html + 'include' => ['@compiler_optimized'], // https://cs.symfony.com/doc/rules/function_notation/native_function_invocation.html#include + 'scope' => 'namespaced', // https://cs.symfony.com/doc/rules/function_notation/native_function_invocation.html#scope + 'strict' => true // https://cs.symfony.com/doc/rules/function_notation/native_function_invocation.html#strict + ], + ]) +->setFinder($finder); diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..becedd2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,213 @@ +# the different stages of this Dockerfile are meant to be built into separate images +# https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage +# https://docs.docker.com/compose/compose-file/#target + + +# https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact +ARG PHP_VERSION=8.1 +ARG CADDY_VERSION=2 + +# yarn build +FROM gmolaire/yarn AS yarn_build +WORKDIR /usr/app +RUN apt-get update && apt-get install tar +RUN mkdir -p /usr/app/vendor/symfony +RUN curl -L https://github.com/symfony/ux-autocomplete/archive/v2.7.1.tar.gz -o ux-autocomplete.tar.gz +RUN tar -xzvf ux-autocomplete.tar.gz --directory /usr/app/vendor/symfony +RUN mv /usr/app/vendor/symfony/ux-autocomplete-2.7.1 /usr/app/vendor/symfony/ux-autocomplete +COPY package.json yarn.lock . +RUN yarn install +COPY . . +RUN yarn build + +# Prod image +FROM php:${PHP_VERSION}-fpm-alpine AS app_php + +# needed for security update until base image is updated +RUN apk upgrade libcurl curl openssl openssl-dev libressl libcrypto3 libssl3 + +# Allow to use development versions of Symfony +ARG STABILITY="stable" +ENV STABILITY ${STABILITY} + +# Allow to select Symfony version +ARG SYMFONY_VERSION="" +ENV SYMFONY_VERSION ${SYMFONY_VERSION} + +ENV APP_ENV=prod + +WORKDIR /srv/app + +# php extensions installer: https://github.com/mlocati/docker-php-extension-installer +ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ +RUN chmod +x /usr/local/bin/install-php-extensions + +# persistent / runtime deps +RUN apk add --no-cache \ + acl \ + fcgi \ + file \ + gettext \ + git \ + nghttp2 \ + libcrypto3 \ + libssl3 \ + ; + +RUN set -eux; \ + install-php-extensions \ + intl \ + zip \ + apcu \ + opcache \ + xsl \ + redis \ + bcmath \ + ; + +###> doctrine/doctrine-bundle ### +RUN apk add --no-cache --virtual .pgsql-deps postgresql-dev; \ + docker-php-ext-install -j$(nproc) pdo_pgsql; \ + apk add --no-cache --virtual .pgsql-rundeps so:libpq.so.5; \ + apk del .pgsql-deps +###< doctrine/doctrine-bundle ### +###< recipes ### + +RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" +COPY docker/php/conf.d/app.ini $PHP_INI_DIR/conf.d/ +COPY docker/php/conf.d/app.prod.ini $PHP_INI_DIR/conf.d/ + +COPY docker/php/php-fpm.d/zz-docker.conf /usr/local/etc/php-fpm.d/zz-docker.conf +RUN mkdir -p /var/run/php + +COPY docker/php/docker-healthcheck.sh /usr/local/bin/docker-healthcheck +RUN chmod +x /usr/local/bin/docker-healthcheck + +HEALTHCHECK --interval=10s --timeout=3s --retries=3 CMD ["docker-healthcheck"] + +COPY docker/php/docker-entrypoint.sh /usr/local/bin/docker-entrypoint +RUN chmod +x /usr/local/bin/docker-entrypoint + +ENTRYPOINT ["docker-entrypoint"] +CMD ["php-fpm"] + +# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser +ENV COMPOSER_ALLOW_SUPERUSER=1 +ENV PATH="${PATH}:/root/.composer/vendor/bin" + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +RUN chown -R www-data: /srv/app/ /var/run/php/ + +# prevent the reinstallation of vendors at every changes in the source code +USER www-data +COPY --chown=www-data:www-data composer.* symfony.* ./ +RUN set -eux; \ + if [ -f composer.json ]; then \ + composer install --prefer-dist --no-dev --no-autoloader --no-scripts --no-progress; \ + composer clear-cache; \ + fi + +# copy sources +COPY --chown=www-data:www-data . . +RUN rm -Rf docker/ + +RUN set -eux; \ + mkdir -p var/cache var/log; \ + if [ -f composer.json ]; then \ + composer dump-autoload --classmap-authoritative --no-dev; \ + composer dump-env prod; \ + COMPOSER_MEMORY_LIMIT=-1 composer run-script --no-dev post-install-cmd; \ + chmod +x bin/console; sync; \ + fi; + +# copy yarn build output +COPY --from=yarn_build --chown=www-data:www-data /usr/app/public/build/ public/build/ + +# Dev image +FROM app_php AS app_php_dev + +USER root + +###> recipes ### +###> symfony/panther ### +# Chromium and ChromeDriver +ENV PANTHER_NO_SANDBOX 1 +# Not mandatory, but recommended +ENV PANTHER_CHROME_ARGUMENTS='--disable-dev-shm-usage' +RUN apk add --no-cache chromium chromium-chromedriver + +# Firefox and geckodriver +#ARG GECKODRIVER_VERSION=0.29.0 +#RUN apk add --no-cache firefox +#RUN wget -q https://github.com/mozilla/geckodriver/releases/download/v$GECKODRIVER_VERSION/geckodriver-v$GECKODRIVER_VERSION-linux64.tar.gz; \ +# tar -zxf geckodriver-v$GECKODRIVER_VERSION-linux64.tar.gz -C /usr/bin; \ +# rm geckodriver-v$GECKODRIVER_VERSION-linux64.tar.gz +###< symfony/panther ### + + +# Additional dev tools (graphviz is to have the dot program to generate the workflows' graphs) +RUN apk add --no-cache \ + make \ + nano \ + vim \ + neovim \ + graphviz \ + ; + +# Load aliases at interactive shell (sh -l) +ENV ENV="/etc/profile" + +ENV APP_ENV=dev XDEBUG_MODE=off +USER www-data +# We must create directories to avoid problems with EasyAdmin which checks rights +# even a cloud storgae is used (thoses directories will stay empty when a cloud +# storage is used). +RUN mkdir -p /srv/app/public/storage/uploads/category +RUN mkdir -p /srv/app/public/storage/uploads/menu +RUN mkdir -p /srv/app/public/storage/uploads/product +RUN mkdir -p /srv/app/public/storage/uploads/user + +USER root +VOLUME /srv/app/var/ + +RUN rm $PHP_INI_DIR/conf.d/app.prod.ini; \ + mv "$PHP_INI_DIR/php.ini" "$PHP_INI_DIR/php.ini-production"; \ + mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" + +COPY docker/php/conf.d/app.dev.ini $PHP_INI_DIR/conf.d/ + +RUN set -eux; \ + install-php-extensions xdebug + +RUN rm -f .env.local.php + +# Build Caddy with the Mercure and Vulcain modules +# FROM caddy:${CADDY_VERSION}-builder-alpine AS app_caddy_builder + +# Temporary fix for https://github.com/dunglas/mercure/issues/770 +# https://github.com/dunglas/symfony-docker/pull/407/files + +FROM caddy:2.7-builder-alpine AS app_caddy_builder + +# RUN xcaddy build \ +# --with github.com/dunglas/mercure \ +# --with github.com/dunglas/mercure/caddy \ +# --with github.com/dunglas/vulcain \ +# --with github.com/dunglas/vulcain/caddy + +RUN xcaddy build \ + --with github.com/dunglas/mercure/caddy \ + --with github.com/dunglas/vulcain/caddy + +# Caddy image +FROM caddy:${CADDY_VERSION} AS app_caddy + +# needed for security update until base image is updated +RUN apk upgrade libcurl curl openssl openssl-dev libressl libcrypto1.1 libssl1.1 libcrypto3 libssl3 + +WORKDIR /srv/app + +COPY --from=app_caddy_builder /usr/bin/caddy /usr/bin/caddy +COPY --from=app_php /srv/app/public public/ +COPY docker/caddy/Caddyfile /etc/caddy/Caddyfile diff --git a/ERRORS.md b/ERRORS.md new file mode 100644 index 0000000..46d7968 --- /dev/null +++ b/ERRORS.md @@ -0,0 +1,180 @@ +# Errors + +This documention gives solutions for some errors occuring in the project. + +## 1. Makefile + +### Error 1.1 + + plateformcoop-ebs$ docker compose exec php ./vendor/bin/php-cs-fixer fix --allow-risky=yes + OCI runtime exec failed: exec failed: unable to start container process: exec: "./vendor/bin/php-cs-fixer": permission denied: unknown + +### Solution 1.1 + +Verify that the executables in `vendor/bin`have the executblale right: + + cd /srv/app/vendor/bin; chmod +x * + + +## 2. PHPStan + +### Error 2.1 + + tests/Integration/Translator/NotranslatorTest.php + --------------------------------------------------------------------------- + Service "App\Translator\NoTranslator" is not registered in the container. + --------------------------------------------------------------------------- + +### Solution 2.1 + +To resolve this error, run: + + make stan-cc + + +## 3. PHPUnit + +### Error 3.1 + + ..........................................make: *** [Makefile:159: test] Error 137 + +### Solution 3.1 + +Check if there is no `dump()` in a loop. + +### Error 3.2 + + ..................make: *** [test] Error 139 + +### Solution 3.2 + +It seems to be a temporary Docker error. Run again the tests. Try the debug mode +`make test-debug` if the error is still there to find the faulty tests. + + +### Error 3.3 + +``` +1) App\Tests\Functional\Controller\User\ServiceRequest\ServiceRequestStatusWorkflowControllerRefuseTest::testTransitionRefuseSuccess + +LogicException: The selected node does not have a form ancestor. + +/srv/app/vendor/symfony/dom-crawler/Form.php:372 +/srv/app/vendor/symfony/dom-crawler/AbstractUriElement.php:45 +/srv/app/vendor/symfony/dom-crawler/Form.php:38 +/srv/app/vendor/symfony/dom-crawler/Crawler.php:838 +/srv/app/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowControllerRefuseTest.php:35 +``` + +It's because we have the same traduction in two places. + +``` +
+ +
+ + + +``` + +### Solution 3.2 + +``` +
+ +
+``` + +Change the traduction key for the first button + + +## 4. Geocoding/Nominatim + +### Error 4.1 + +Error for all the geocoding tests (poc or modify my address): + + ErrorException: Handling "App\Message\Query\Admin\User\UserAddressQuery" failed: Serialization of 'Closure' is not allowed + in /srv/app/vendor/symfony/messenger/Middleware/HandleMessageMiddleware.php:129 + +It is because the Nominatim service is down (error 502, bad gateway). +Disable the cache in `config/packages/bazinga_geocoder.yaml` to see the real error: + + The geocoder server returned an invalid response (502) for query "https://nominatim.openstreetmap.org/search?format=jsonv2&q=Timipi%2C%20Fives%2C%20france&addressdetails=1&extratags=1&limit=3&accept-language=fr". + We could not parse it. + + https://nominatim.openstreetmap.org/search?format=jsonv2&q=Timipi%2C%20Fives%2C%20france&addressdetails=1&extratags=1&limit=3&accept-language=fr" + + 502 Bad Gateway + nginx + +### Solution 4.1 + +Wait. + +It would be nice if the bundle could hande this error correclty and at least return +an empty results array if there is such an error. +As it is made now, we can't catch this error, which is quite problematic. +Create a new issue on the bundle to see what can be done. + +### Todo for 4.1 + +Use mocks for the test env. + + +## 5. Docker + +### Error 5.1 + +When running a command with make like `make cs` we have he error: + + OCI runtime exec failed: exec failed: unable to start container process: read init-p: connection reset by peer: unknown + +### Solution 5.1 + +Restart Docker. + + +## 6. Symfony + +### Error 6.1 + +When trying to access a controller we just created: + + Could not resolve argument $productId of "App\Controller\User\Product\DeleteProductAction::__invoke()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"? + +### Solution 6.1 + +There is a mismatch between the arguments of the controller action and the route +requirements, fix them (eg: `productId` instead of `id`. + + + +## 7. Meilisearch + +### Error 7.1 + +When indexing a document: + + The primary key inference failed as the engine found 2 fields ending with `id` in their names: 'id' and 'ownerId'. + Please specify the primary key manually using the `primaryKey` query parameter? + +### Solution 6.1 + +The primary key must specified to avoid confusion. It can be set using the second +argument of the `addDocument()` functions. + + $this->getIndex()->addDocuments([$this->normalizeProduct($product)], self::PRIMARY_KEY); diff --git a/LICENSE b/LICENSE index 0ad25db..bc6b2a1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,661 +1,19 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. +Copyright (c) Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..30aecf9 --- /dev/null +++ b/Makefile @@ -0,0 +1,233 @@ +# Config +CURRENT_UID := $(shell id -u) +CURRENT_GID := $(shell id -g) + +SHELL = sh +REDIS_PORT = 6389 + +# Executables (local) +DOCKER = docker +DOCKER_COMP = docker compose +REDIS = redis-cli + +# Docker containers +PHP_CONT = $(DOCKER_COMP) exec php + +# see https://hub.docker.com/r/gmolaire/yarn +YARN_CONT = $(DOCKER) run -it --rm -w "/usr/app" -v "${PWD}":/usr/app gmolaire/yarn yarn + +# Main executables +PHP = $(PHP_CONT) php +COMPOSER = $(PHP_CONT) composer +SYMFONY = $(PHP_CONT) bin/console +PHPUNIT = $(PHP_CONT) bin/phpunit + +# Vendors executables +PHPSTAN = $(PHP_CONT) ./vendor/bin/phpstan +PHP_CS_FIXER = $(PHP_CONT) ./vendor/bin/php-cs-fixer +TWIGCS = $(PHP_CONT) ./vendor/bin/twigcs +RECTOR = $(PHP_CONT) ./vendor/bin/rector + +# Misc +.DEFAULT_GOAL = help +.PHONY = help build up start down logs sh composer vendor sf cc ci cs lint-twigcs + +## —— 🎵 🐳 The Symfony Docker Makefile 🐳 🎵 ————————————————————————————————— +help: ## Outputs this help screen + @grep -E '(^[a-zA-Z0-9_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}{printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/' + +## —— Docker 🐳 ———————————————————————————————————————————————————————————————— +build: ## Builds the Docker images + @$(DOCKER_COMP) build --pull + +up: ## Start the docker hub in detached mode (no logs) + @$(DOCKER_COMP) up -d + +wait: ## Waits for all containers to be ready + @$(DOCKER_COMP) up --wait + +start: ## Starts the main containers (load-fixtures must be done so the caddy service can be healthy) +start: up yarn-install load-fixtures yarn-dev wait + +start-dev: ## Start additional and optional dev tools in the docker-compose.override.yml.dist file + @$(DOCKER_COMP) -f docker-compose.override.yml.dist up --wait + +stop: ## Stop and remove the docker containers (and volumes) of the project + @$(DOCKER_COMP) down -v --remove-orphans + +logs: ## Show live logs + @$(DOCKER_COMP) logs --tail=0 --follow + +sh: ## Connect to the PHP FPM container + @$(PHP_CONT) sh -l + +linux-fix-perms: ## Fix permissions on Linux + $(PHP_CONT) chown -R $(CURRENT_UID):$(CURRENT_GID) . + +## —— Composer 🧙 —————————————————————————————————————————————————————————————— +composer: ## Run composer, pass the parameter "c=" to run a given command, example: make composer c='req symfony/orm-pack' + @$(eval c ?=) + @$(COMPOSER) $(c) + +vendor: composer.lock ## Install vendors according to the current composer.lock file +vendor: c=install --prefer-dist --no-progress --no-scripts --no-interaction +vendor: composer + +## —— Symfony 🎵 ——————————————————————————————————————————————————————————————— +sf: ## List all Symfony commands or pass the parameter "c=" to run a given command, example: make sf c=about + @$(eval env ?= 'dev') + @$(eval c ?=) + @$(SYMFONY) $(c) --env=$(env) + +cc: c=c:c ## Clear the cache +cc: sf + +purge: ## Purge the all vars files manually + @$(PHP_CONT) sh -c "rm -rf /srv/app/var/*" + +## —— Coding standards ✨ —————————————————————————————————————————————————————— +stan: ## Run PHPStan. Use the 'path' parameter to check a given file only: make stan path=src/Services/ControllerHelper.php + $(eval path ?= ) + @$(PHPSTAN) analyse -c phpstan.neon --memory-limit 1G -vvv $(path) + +stan-cc: ## Clear manually the PHPStan result cache + @$(PHPSTAN) clear-result-cache + +lint-php: ## Lint files with php-cs-fixer (just use fix-php) + $(PHP_CS_FIXER) fix --dry-run --allow-risky=yes + +lint-twig: ## Lint Twig files to check they are well formatted (no parse error) + @$(SYMFONY) lint:twig --env=dev + +lint-twigcs: ## Check Twig coding style + @$(TWIGCS) templates/ --exclude vendor + +lint-container: ## Check there is no problem with the container + @$(SYMFONY) lint:container + +lint-yaml: ## Check all YAML files are well formatted + @$(SYMFONY) lint:yaml --parse-tags config/ translations/ templates/ + +fix-php: ## Fix files with php-cs-fixer + @$(PHP_CS_FIXER) fix --allow-risky=yes + +rector: ## Run rector with current rules in rector.php + @$(RECTOR) process src/ + +ci: ## Run pre-commit checks to ensure the CI will be green +ci: cs lint-yaml lint-container lint-twig lint-twigcs yarn-lint doctrine-validate test-complete + +cs: ## Run PHPStan and php-cs-fixer only +cs: fix-php stan + +## —— Project —————————————————————————————————————————————————————————————————— +load-fixtures: drop-db ## Build the DB, control the schema validity, load fixtures and check the migration status (deb) + $(eval env ?= 'dev') + @$(SYMFONY) doctrine:database:create --if-not-exists --env=$(env) + @$(SYMFONY) doctrine:schema:create --env=$(env) + @$(SYMFONY) doctrine:schema:validate --env=$(env) + @$(SYMFONY) doctrine:migrations:migrate --env=$(env) --no-interaction + @$(SYMFONY) messenger:setup-transports --env=$(env) + @$(SYMFONY) hautelook:fixtures:load --no-interaction -vv --no-bundles --env=$(env) + @$(SYMFONY) app:index-products --env=$(env) + +drop-db: ## Delete the whole database, useful when having integrity problems for data or constraints + $(eval env ?= 'dev') + @$(SYMFONY) doctrine:database:drop --env=$(env) --if-exists --force + +load-test-fixtures: env=test ## Allows to use the test fixtures to debug problems +load-test-fixtures: load-fixtures + +load-prod-fixtures: env=prod +load-prod-fixtures: load-fixtures ## Same than load-fixtures but with only minimum data + +cache-clear: ## Clear the application cache (used for tests) + $(eval env ?= 'dev') + @$(SYMFONY) c:c --env=$(env) + @$(SYMFONY) cache:pool:clear cache.app --env=$(env) + +doctrine-validate: ## Validate the doctrine schema + $(eval env ?= 'dev') + @$(SYMFONY) doctrine:schema:validate --env=$(env) + +doctrine-migrate: ## Run all the available Doctrine migrations + @$(SYMFONY) doctrine:migrations:migrate --no-interaction + +meilisearch-index: + $(eval env ?= 'dev') + @$(SYMFONY) app:index-products --env=$(env) + + +## —— Tests ✅ ————————————————————————————————————————————————————————————————— +test: ## Run tests with optional suite, filter and options + @$(eval testsuite ?= 'all') + @$(eval filter ?= '.') + @$(eval options ?= '--stop-on-failure') + @$(PHPUNIT) --testsuite=$(testsuite) --filter=$(filter) $(options) + +test-complete: ## Run all tests without stopping on the first error +test-complete: env=test +test-complete: options= +test-complete: cc load-fixtures test + +test-debug: ## Run all tests in debug mode +test-debug: env=test +test-debug: options=--debug --stop-on-failure +test-debug: cc load-fixtures test + +test-unit: ## Run unit tests only +test-unit: env=test +test-unit: testsuite=unit +test-unit: test + +test-api: ## Run API tests only +test-api: env=test +test-api: testsuite=api +test-api: test + +test-integration: ## Run integration tests only +test-integration: env=test +test-integration: testsuite=integration +test-integration: load-test-fixtures test + +test-functional: ## Run functional tests only +test-functional: env=test +test-functional: testsuite=functional +test-functional: load-test-fixtures test + +test-e2e: ## Run E2E tests only +test-e2e: env=test +test-e2e: load-test-fixtures test + +coverage: ## Generate the HTML PHPUnit code coverage report locally +coverage: env=test +coverage: load-test-fixtures + # Cache must be generated by PHPUnit so it can run compiler passes + @$(PHP_CONT) sh -c "rm -rf /srv/app/var/cache/test" + @$(DOCKER_COMP) exec -e XDEBUG_MODE=coverage php php -d xdebug.enable=1 -d memory_limit=-1 bin/phpunit --coverage-html=docs/coverage + +## —— Debug 🐞—————————————————————————————————————————————————————————————————— +redis: ## Connect to redis with CLI (redis-cli must be installed locally) + @$(REDIS) -p $(REDIS_PORT) + +redis-cc: ## Flush all Redis cache + @$(REDIS) -p $(REDIS_PORT) flushall + +## —— Yarn 🐱 / JavaScript ————————————————————————————————————————————————————— +yarn-install: ## Install node dependencies with Yarn + @$(YARN_CONT) install + +yarn-dev: ## Build the assets for the dev env + @$(YARN_CONT) dev + +yarn-lint: ## Lint JS files + @$(YARN_CONT) lint + +yarn-cmd: ## Run a given command + @$(eval cmd ?= 'help') + @$(YARN_CONT) $(cmd) + +## —— Doc 📚 ——————————————————————————————————————————————————————————————————— +workflows: ## Generate and update the graphs of all available workflows + @$(PHP_CONT) bin/console workflow:dump service_request_status | dot -Tpng -o docs/service_request_status_workflow.png + @echo "Done!" diff --git a/README.md b/README.md index 37f4280..16d34b6 100644 --- a/README.md +++ b/README.md @@ -205,4 +205,4 @@ La page en question bénéficiera de l’accès au reste du framework, à la bas # Installation et documentation technique -La documentation d'installation et configuration technique de la plateforme est disponible sur [cette page](). +La documentation d'installation et configuration technique de la plateforme est disponible sur [cette page](docs/README.md). diff --git a/assets/app.js b/assets/app.js new file mode 100644 index 0000000..0b36e93 --- /dev/null +++ b/assets/app.js @@ -0,0 +1,18 @@ + +/* + * Welcome to your app's main JavaScript file! + * + * We recommend including the built version of this JavaScript file + * (and its CSS file) in your base layout (base.html.twig). + */ + +// any CSS you import will output into a single css file (app.css in this case) +import './styles/global.scss' + +// start the Stimulus application +import './stimulus' + +import '@fortawesome/fontawesome-free/js/fontawesome' +import '@fortawesome/fontawesome-free/js/solid' +import '@fortawesome/fontawesome-free/js/regular' +import '@fortawesome/fontawesome-free/js/brands' diff --git a/assets/controllers.json b/assets/controllers.json new file mode 100644 index 0000000..1f6fc3a --- /dev/null +++ b/assets/controllers.json @@ -0,0 +1,15 @@ +{ + "controllers": { + "@symfony/ux-autocomplete": { + "autocomplete": { + "enabled": true, + "fetch": "eager", + "autoimport": { + "tom-select/dist/css/tom-select.default.css": true, + "tom-select/dist/css/tom-select.bootstrap5.css": false + } + } + } + }, + "entrypoints": [] +} diff --git a/assets/controllers/accordion_controller.js b/assets/controllers/accordion_controller.js new file mode 100644 index 0000000..bc90d26 --- /dev/null +++ b/assets/controllers/accordion_controller.js @@ -0,0 +1,9 @@ +import { Controller } from '@hotwired/stimulus' + +export default class extends Controller { + toggle() { + const accordionList = this.element.nextElementSibling + this.element.lastElementChild.classList.toggle('rotate-180') + accordionList.classList.toggle('opened') + } +} diff --git a/assets/controllers/account_controller.js b/assets/controllers/account_controller.js new file mode 100644 index 0000000..8eb940d --- /dev/null +++ b/assets/controllers/account_controller.js @@ -0,0 +1,65 @@ +import { Controller } from '@hotwired/stimulus' + +export default class extends Controller { + + hiddenUserInput () { + const divUserInput = document.querySelector('.user-input') + divUserInput.classList.add('hidden') + } + + hiddenPlaceInput() { + const divUserInput = document.querySelector('.place-input') + divUserInput.classList.add('hidden') + } + connect () { + const userInput = this.element.value === 'user' + const placeInput = this.element.value === 'place' + const inputUserLastname = document.querySelector('.input-lastname') + const inputUserFirstname = document.querySelector('.input-firstname') + const inputPlaceName = document.querySelector('.input-name') + + const userInputChecked = userInput && this.element.checked + const placeInputChecked = placeInput && this.element.checked + + if (userInputChecked) { + inputUserFirstname.required = true + inputUserLastname.required = true + this.hiddenPlaceInput() + } + + if (placeInputChecked) { + inputPlaceName.required = true + this.hiddenUserInput() + } + } + + choosenType() { + const placeInput = this.element.value === 'place' + const inputUserLastname = document.querySelector('.input-lastname') + const inputUserFirstname = document.querySelector('.input-firstname') + const inputPlaceName = document.querySelector('.input-name') + + const divUserInput = document.querySelector('.user-input') + const divPlaceInput = document.querySelector('.place-input') + + const placeInputChecked = placeInput && this.element.checked + + if (placeInputChecked) { + inputUserFirstname.removeAttribute('required') + inputUserLastname.removeAttribute('required') + + inputPlaceName.setAttribute('required', '') + + divPlaceInput.classList.remove('hidden') + divUserInput.classList.add('hidden') + } else { + inputPlaceName.removeAttribute('required') + + inputUserFirstname.setAttribute('required', '') + inputUserLastname.setAttribute('required', '') + + divUserInput.classList.remove('hidden') + divPlaceInput.classList.add('hidden') + } + } +} diff --git a/assets/controllers/calendar_controller.js b/assets/controllers/calendar_controller.js new file mode 100644 index 0000000..9e9acec --- /dev/null +++ b/assets/controllers/calendar_controller.js @@ -0,0 +1,114 @@ +import { Controller} from '@hotwired/stimulus' +import flatpickr from 'flatpickr' +import rangePlugin from 'flatpickr/dist/plugins/rangePlugin' +import { French } from 'flatpickr/dist/l10n/fr' +export default class extends Controller { + static values = { + unavailabilities: String + } + + initialize() { + this.fp + this.fpProductOwner + } + + connect() { + const unavailabilities = this.unavailabilitiesValue.split(',') + + const selector = this.element.querySelector('#calendar-start-day') + + const commonOptions = { + locale: { + ...French, + weekdays: { + shorthand: ['D', 'L', 'M', 'M', 'J', 'V', 'S'], // Override shorthand because it is initially in the "lun", "mar", "mer" (etc) format + longhand: French.weekdays.longhand, + } + }, + inline: true, + disable: unavailabilities, + monthSelectorType: 'static', + minDate: 'today', + } + + if (!selector) { + // This flatpickr instance is meant for when a user visits its own product page + this.fpProductOwner = flatpickr('#product-owner-calendar', commonOptions) + + // This prevents owner from selecting days + this.fpProductOwner.daysContainer.addEventListener('click', (event) => { + event.stopPropagation() + }, true) + + return + } + + this.fp = flatpickr(selector, { + ...commonOptions, + allowInput: true, + mode: 'range', + plugins: [new rangePlugin({ input: '#calendar-end-day'})], + onReady(_, __, instance) { + let params = (new URL(location)).searchParams + const startAt = params.get('startAt') + const endAt = params.get('endAt') + + const dates = [] + + if (startAt) dates.push(startAt) + if (endAt) dates.push(endAt) + + if (dates.length) { + instance.setDate(dates, true) + return + } + + const buttonServiceRequestPage = document.querySelector('#create_service_request_submit') + + if (!buttonServiceRequestPage) return + + buttonServiceRequestPage.setAttribute('disabled', '') + }, + onChange() { + const startAtInput = document.querySelector('#calendar-start-day') + const endAtInput = document.querySelector('#calendar-end-day') + const startAt = startAtInput.value + const endAt = endAtInput.value + + const buttonProductPage = document.querySelector('#service-request') + const buttonServiceRequestPage = document.querySelector('#create_service_request_submit') + + if (buttonProductPage) { + startAt && endAt + ? buttonProductPage.removeAttribute('disabled') + : buttonProductPage.setAttribute('disabled', '') + } + + if (buttonServiceRequestPage) { + startAt && endAt + ? buttonServiceRequestPage.removeAttribute('disabled') + : buttonServiceRequestPage.setAttribute('disabled', '') + } + } + }) + } + + resetDates() { + this.fp.clear() + } + + serviceRequest() { + const button = this.element.querySelector('#service-request') + const path = button.dataset.path + const startAtInput = this.element.querySelector('#calendar-start-day') + const endAtInput = this.element.querySelector('#calendar-end-day') + const startAt = startAtInput.value + const endAt = endAtInput.value + + const url = new URL(path, window.location.origin) + url.searchParams.set('startAt', startAt) + url.searchParams.set('endAt', endAt) + + location.href = url + } +} diff --git a/assets/controllers/menu_controller.js b/assets/controllers/menu_controller.js new file mode 100644 index 0000000..cf9c025 --- /dev/null +++ b/assets/controllers/menu_controller.js @@ -0,0 +1,8 @@ +import {Controller} from '@hotwired/stimulus' + +export default class extends Controller { + toggle() { + const menu = document.querySelector('.menu') + menu.classList.toggle('hide') + } +} diff --git a/assets/controllers/product_controller.js b/assets/controllers/product_controller.js new file mode 100644 index 0000000..fab2a2b --- /dev/null +++ b/assets/controllers/product_controller.js @@ -0,0 +1,34 @@ +import { Controller} from '@hotwired/stimulus' +import { Toast } from 'bootstrap' + +export default class extends Controller { + static targets = [ 'activeButton', 'pausedButton', 'activeTag', 'pausedTag' ] + + static values = { + route: String, + } + + async switchStatus() { + const response = await fetch(this.routeValue, { method: 'PATCH' }) + + if (!response.ok) { + const toastElement = document.querySelector('[data-notification=error]') + const toast = new Toast(toastElement) + toast.show() + return + } + + const data = await response.json() + const { status } = data + + const toastElement = document.querySelector(`[data-notification=${status}StatusSuccess]`) + const toast = new Toast(toastElement) + toast.show() + + this.activeButtonTarget.classList.toggle('d-none') + this.pausedButtonTarget.classList.toggle('d-none') + + this.activeTagTarget.classList.toggle('d-none') + this.pausedTagTarget.classList.toggle('d-none') + } +} diff --git a/assets/controllers/productupload_controller.js b/assets/controllers/productupload_controller.js new file mode 100644 index 0000000..46ff046 --- /dev/null +++ b/assets/controllers/productupload_controller.js @@ -0,0 +1,40 @@ +import { Controller} from '@hotwired/stimulus' + +export default class extends Controller { + static targets = ['input', 'feedback'] + + static values = { + uploadMaxsizeByFile: Number, // max size by image allowed + uploadMaxItems: Number, // max number of images allowed + currentImagesCount: Number, // number of images already uploaded + feedbackMessage: String, // image too too big feedback + maxImagesError: String, // count threshold reached error + } + + /** + * 1. Check the size of each file. Remove the selected files if one is incorrect. + * 2. Check the number of images uploaded. + */ + checkUpload() { + const maxAllowedImages = this.uploadMaxItemsValue - this.currentImagesCountValue + if (maxAllowedImages < this.inputTarget.files.length) { + this.feedbackTarget.innerHTML = this.maxImagesErrorValue + this.inputTarget.value = '' + + return + } + + const uploadMaxsizeByFile = 1048576 * this.uploadMaxsizeByFileValue + let feedback = [] + for (let file of this.inputTarget.files) { + if (file.size > uploadMaxsizeByFile) { + feedback.push(this.feedbackMessageValue.replace('%file%', file.name)) + } + } + + if (feedback.length > 0) { + this.feedbackTarget.innerHTML = feedback.join('
') + this.inputTarget.value = '' + } + } +} diff --git a/assets/controllers/productvisibility_controller.js b/assets/controllers/productvisibility_controller.js new file mode 100644 index 0000000..c80a5dc --- /dev/null +++ b/assets/controllers/productvisibility_controller.js @@ -0,0 +1,18 @@ +import { Controller} from '@hotwired/stimulus' + +export default class extends Controller { + static targets = ['public', 'groups'] + + connect () { + if (this.publicTarget.checked) { + this.hideGroups() + } + } + + hideGroups() { + this.groupsTarget.classList.add('hidden') + } + showGroups() { + this.groupsTarget.classList.remove('hidden') + } +} diff --git a/assets/controllers/read-more_controller.js b/assets/controllers/read-more_controller.js new file mode 100644 index 0000000..475a95b --- /dev/null +++ b/assets/controllers/read-more_controller.js @@ -0,0 +1,14 @@ +import {Controller} from '@hotwired/stimulus' + +export default class extends Controller { + toggle() { + const readMoreContainer = document.querySelector('.read') + const buttonToggle = this.element + const opened = readMoreContainer.classList.contains('less') + + readMoreContainer.classList.toggle('less') + readMoreContainer.classList.toggle('more') + + buttonToggle.innerHTML = opened ? 'Voir plus' : 'Voir moins' + } +} diff --git a/assets/controllers/search_controller.js b/assets/controllers/search_controller.js new file mode 100644 index 0000000..56ca318 --- /dev/null +++ b/assets/controllers/search_controller.js @@ -0,0 +1,16 @@ +import { Controller} from '@hotwired/stimulus' + +export default class extends Controller { + static targets = [ 'form' ] + + /** + * Reset the form to blank values (input reset doesn't work with prefilled values). + */ + reset() { + this.formTarget.elements['p[q]'].value = '' + this.formTarget.elements['p[category]'].value = '' + this.formTarget.elements['p[place]'].value = '' + this.formTarget.elements['p[city]'].value = '' + this.formTarget.elements['p[distance]'].forEach(radio => radio.checked = false) + } +} diff --git a/assets/controllers/tooltips_controller.js b/assets/controllers/tooltips_controller.js new file mode 100644 index 0000000..1b6531d --- /dev/null +++ b/assets/controllers/tooltips_controller.js @@ -0,0 +1,11 @@ +import { Controller } from '@hotwired/stimulus' +import { Tooltip } from 'bootstrap' + +export default class extends Controller { + connect() { + const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) + tooltipTriggerList.map(function (tooltipTriggerEl) { + return new Tooltip(tooltipTriggerEl) + }) + } +} diff --git a/assets/stimulus.js b/assets/stimulus.js new file mode 100644 index 0000000..c02d609 --- /dev/null +++ b/assets/stimulus.js @@ -0,0 +1,21 @@ +import { startStimulusApp } from '@symfony/stimulus-bridge' +import { Application } from '@hotwired/stimulus' + +import PasswordVisibility from 'stimulus-password-visibility' +import Carousel from 'stimulus-carousel' +import 'swiper/swiper-bundle.css' + +// Registers Stimulus controllers from controllers.json and in the controllers/ directory +export const app = startStimulusApp(require.context( + '@symfony/stimulus-bridge/lazy-controller-loader!./controllers', + true, + /\.[jt]sx?$/ +)) + +// register any custom, 3rd party controllers here +// app.register('some_controller_name', SomeImportedController); + +const application = Application.start() +application.register('carousel', Carousel) +application.register('password-visibility', PasswordVisibility) + diff --git a/assets/styles/_accordion.scss b/assets/styles/_accordion.scss new file mode 100644 index 0000000..f5d39e6 --- /dev/null +++ b/assets/styles/_accordion.scss @@ -0,0 +1,18 @@ +.accordion.rotate-180 { + transition: transform .3s ease-in-out; +} + +.accordion { + transition: transform .3s ease-in-out; +} + +.accordion-list { + max-height: 0; + transition: max-height .3s ease-out; + overflow: hidden; + + &.opened { + max-height: 500px; + transition: max-height .3s ease-in; + } +} diff --git a/assets/styles/_address.scss b/assets/styles/_address.scss new file mode 100644 index 0000000..6c7e675 --- /dev/null +++ b/assets/styles/_address.scss @@ -0,0 +1,18 @@ +.address-card { + display: flex; + align-items: center; + + &-checkbox { + width: fit-content; + } + + &-info { + display: flex; + flex-direction: column; + + > p { + margin: 0; + color: $heading-color; + } + } +} diff --git a/assets/styles/_calendar.scss b/assets/styles/_calendar.scss new file mode 100644 index 0000000..b9e6486 --- /dev/null +++ b/assets/styles/_calendar.scss @@ -0,0 +1,73 @@ +.flatpickr-calendar { + align-self: center; + + &::before, + &::after { + display: none; + } + + .cur-month, + .cur-year { + font-size: $font-size-base !important; + font-weight: $font-weight-normal !important; + } + + .cur-month { + text-transform: capitalize; + } + + .cur-year { + pointer-events: none; + } + + .arrowUp, + .arrowDown { + display: none; + } +} + +.flatpickr-current-month { + padding-top: 10px; +} + +.flatpickr-weekdays { + margin-top: $spacer; +} + +.flatpickr-weekday { + font-weight: $font-weight-normal !important; + color: $gray-600 !important; +} + +.flatpickr-day { + color: $primary !important; + + &.selected { + border: none; + border-radius: 4px !important; + background: $primary; + color: $white !important; + } + + &.inRange { + color: $black !important; + } + + &.prevMonthDay, + &.nextMonthDay { + color: rgba($gray-600, .5) !important; + } +} + +.flatpickr-disabled { + color: $gray-600 !important; + text-decoration: line-through; +} + +.flatpickr-prev-month, +.flatpickr-next-month { + svg { + fill: $primary; + } +} + diff --git a/assets/styles/_card.scss b/assets/styles/_card.scss new file mode 100644 index 0000000..6df14f8 --- /dev/null +++ b/assets/styles/_card.scss @@ -0,0 +1,22 @@ +.card { + .img-container { + @media (min-width: 1200px) { + height: 310px; + } + @media (max-width: 1025px) { + height: 235px; + } + @media (max-width: 700px) { + height: 180px; + } + } + .img { + object-fit: contain; + width: 100%; + } + + .img-default { + object-fit: cover; + width: 100%; + } +} diff --git a/assets/styles/_chat.scss b/assets/styles/_chat.scss new file mode 100644 index 0000000..04760b3 --- /dev/null +++ b/assets/styles/_chat.scss @@ -0,0 +1,36 @@ +.chat-content { + margin-bottom: 200px; +} + +.chat-action { + width: 100%; +} + +@media screen and (min-width: 992px) { + .chat-container { + max-height: 800px; + overflow-y: auto; + position: relative; + + .chat-content { + margin-bottom: 150px; + } + + } + + .chat-action { + width: 75%; + } + .conversation-textarea { + height: 80px; + } +} + +@media screen and (min-width: 992px) and (max-width: 1199px) { + .chat-content { + margin-bottom: 150px; + } + .chat-action { + width: 66.6%; + } +} diff --git a/assets/styles/_connexion.scss b/assets/styles/_connexion.scss new file mode 100644 index 0000000..ce70f6b --- /dev/null +++ b/assets/styles/_connexion.scss @@ -0,0 +1,81 @@ +#account_create_step2_form_type, +#create_group_form_type +{ + display: flex; + + > .form-check { + margin-right: 10px; + + &:last-child { + margin-right: 0; + } + } +} + +label.required:after, +legend.required:after +{ + content: "*" !important; + color: #B02A37; + padding-left: 4px; +} + +label.form-check-label.required:after { + content: '' !important; +} + +#account_create_step2_form_plainPassword_first:invalid, +#account_create_step2_form_plainPassword_second:invalid, +#login-password:invalid { + background-image: none !important; +} + +#account_create_step2_form_plainPassword_first:valid, +#account_create_step2_form_plainPassword_second:valid, +#login-password:valid { + background-image: none !important; +} + +.password-visibility { + display: flex; + align-items: center; + + > input { + border-right: 0; + } + + > .eye { + background-color: transparent; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: var(--bs-border-radius-sm) !important; + border-top-right-radius: var(--bs-border-radius-sm) !important; + height: fit-content; + padding: 4px 8px 1.2px 4px; + } + + > input.is-invalid ~ .eye { + border: 1px solid #B02A37; + } + + > input.is-invalid ~ span { + display: none; + } +} + +.was-validated { + > .password { + > .password-visibility { + > input:invalid ~ .eye { + border: 1px solid #B02A37; + } + + > input:valid ~ .eye { + border: 1px solid #198754; + } + } + } +} + + + diff --git a/assets/styles/_conversations.scss b/assets/styles/_conversations.scss new file mode 100644 index 0000000..64f7a17 --- /dev/null +++ b/assets/styles/_conversations.scss @@ -0,0 +1,59 @@ +.conversations { + &-link { + transition: background .3s; + + &:hover, + &:focus { + background: rgba($gray-300, 0.3); + color: inherit; + } + } + + &-img { + width: 74px; + aspect-ratio: 1/1; + } +} + +.conversation { + &-message { + border-radius: 4px; + + &.system { + border: 1px solid $gray-400; + } + + &.from_recipient, + &.from_owner { + width: 80%; + } + + &.from_recipient { + background: $blue-100; + } + + &.from_owner { + margin-left: auto; + background: $blue-500; + color: $white; + + .conversation-messageValue { + color: inherit; + } + } + + } + + &-messageValue { + color: $black; + } + + &-textarea { + height: calc(1.5em + 0.75rem + 2px); + } +} + +.label-modal-width.col-sm-2 { + width: 100%; +} + diff --git a/assets/styles/_custom-variables.scss b/assets/styles/_custom-variables.scss new file mode 100644 index 0000000..dc20bde --- /dev/null +++ b/assets/styles/_custom-variables.scss @@ -0,0 +1,23 @@ +$white: #FFFFFF; +$gray: #D4D9DF; +$gray-100: #F8F9FA; +$gray-900: #667080; +$blue-100: #CCE5FF; +$blue-200: #B8DAFF; +$blue-500: #0D6EFD; +$blue-900: #031633; +$red: #B02A37; +$green: #198754; +$yellow: #FFC107; +$gray-border: #CED4DA; + +$dark: $blue-900; +$light: $gray-100; +$primary: $blue-500; + +$heading-color: #152536; +$body-color: #6c757d; + +//FONT +$bolder: 600; +$font-weight-bolder: $bolder; diff --git a/assets/styles/_grid.scss b/assets/styles/_grid.scss new file mode 100644 index 0000000..4d17cc6 --- /dev/null +++ b/assets/styles/_grid.scss @@ -0,0 +1,51 @@ +// GRID PAGE LOAN +.grid-template-loans { + display: grid; + grid-template-areas: + "product calendar" + "message calendar"; + column-gap: 2rem; + grid-template-rows: 100px auto; +} + +// GRID PAGE PRODUCT +.grid-template-product { + display: grid; + grid-template-areas: + "info lender" + "info calendar"; + grid-template-columns: 45rem 3fr; + column-gap: 100px; +} + +.grid { + &-lender { + grid-area: lender; + } + + &-product { + grid-area: product; + } + + &-message { + grid-area: message; + } + + &-calendar { + grid-area: calendar; + } + + &-info { + grid-area: info; + } +} + +@media screen and (max-width: 992px) { + .grid-template-loans, .grid-template-product, .grid-template-conversation { + display: flex; + flex-direction: column; + } + .lender { + display: none; + } +} diff --git a/assets/styles/_group.scss b/assets/styles/_group.scss new file mode 100644 index 0000000..1e755a6 --- /dev/null +++ b/assets/styles/_group.scss @@ -0,0 +1,21 @@ +.card-group { + width: 100%; + + div { + span.badge { + width: fit-content; + } + } +} + +@media screen and (max-width: 991px) { + .group-list { + li { + border-bottom: 1px solid $gray; + + &:last-child { + border: none; + } + } + } +} diff --git a/assets/styles/_headings.scss b/assets/styles/_headings.scss new file mode 100644 index 0000000..a42a117 --- /dev/null +++ b/assets/styles/_headings.scss @@ -0,0 +1,34 @@ +h1, h2, h3, h4, h5, h6 { + color: $heading-color; + font-weight: $font-weight-semibold; +} + +h1 { + font-size: 40px; + font-weight: 600; + line-height: 48.41px; +} + +h2 { + font-size: 32px; + font-weight: 600; + line-height: 38.73px; +} + +h3 { + font-size: 28px; + font-weight: 600; + line-height: 33.89px; +} + +h4 { + font-size: 24px; + font-weight: 600; + line-height: 29.05px; +} + +h5 { + font-size: 20px; + font-weight: 600; + line-height: 24.2px; +} diff --git a/assets/styles/_lender.scss b/assets/styles/_lender.scss new file mode 100644 index 0000000..96f86ab --- /dev/null +++ b/assets/styles/_lender.scss @@ -0,0 +1,11 @@ +.avatar { + width: 24px; + height: 24px; + font-size: 24px; + + &-icon { + &::before { + display: block; + } + } +} diff --git a/assets/styles/_my-account.scss b/assets/styles/_my-account.scss new file mode 100644 index 0000000..777cb98 --- /dev/null +++ b/assets/styles/_my-account.scss @@ -0,0 +1,39 @@ +.myAccount{ + border-bottom: 1px solid $gray-border; + > .myAccount-header { + display: flex; + align-items: center; + > svg { + width: 20px; + height: 20px; + margin-right: 10px; + font-weight: 400; + > path { + fill: $secondary; + } + } + } + + .myAccount-body { + > div { + > svg { + width: 20px; + height: 20px; + > path { + fill: $secondary; + font-weight: lighter; + } + } + } + } +} + +.form-switch { + padding-left: 1em; +} + +@media screen and (min-width: 768px) { + .myAccount { + border-bottom: none; + } +} diff --git a/assets/styles/_pagination.scss b/assets/styles/_pagination.scss new file mode 100644 index 0000000..d0e45e8 --- /dev/null +++ b/assets/styles/_pagination.scss @@ -0,0 +1,41 @@ +._pagination { + display: flex; + align-items: center; + justify-content: space-between; + + > .pagination-items { + padding: 8px 15px; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background-color: rgba(102, 112, 128, 0.25) !important; + } + } + + > .pagination-list { + > ul { + margin: 0; + padding: 0; + display: inline-flex; + > li { + padding: 8px 16px; + margin-right: 8px; + list-style: none; + + &.current { + background-color: rgba(102, 112, 128, 0.25) !important; + } + + &:last-child { + margin-right: 0; + } + + &:hover { + background-color: rgba(102, 112, 128, 0.25) !important; + } + } + } + } +} diff --git a/assets/styles/_product-list.scss b/assets/styles/_product-list.scss new file mode 100644 index 0000000..02eaa5c --- /dev/null +++ b/assets/styles/_product-list.scss @@ -0,0 +1,33 @@ +.product-type { + border-radius: 4px 0 0 4px !important; + border: 1px solid $primary !important; + + &:last-child { + border-radius: 0 4px 4px 0 !important; + } +} + +#p_distance { + display: flex; + justify-content: space-evenly; + + .form-check { + .form-check-label { + margin-left: 8px; + } + } +} + +.input-search { + > input { + border-right: 0; + } + + .search { + background-color: transparent; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: var(--bs-border-radius-sm) !important; + border-top-right-radius: var(--bs-border-radius-sm) !important; + } +} diff --git a/assets/styles/_product.scss b/assets/styles/_product.scss new file mode 100644 index 0000000..528883b --- /dev/null +++ b/assets/styles/_product.scss @@ -0,0 +1,85 @@ +.color-svg-secondary { + > svg { + width: 16px; + height: 16px; + path { + fill: $secondary; + } + } +} + +.product-img{ + max-width: 100%; + max-height: 300px; +} + +.swiper-wrapper { + align-items: center; +} + +.swiper-slide { + display: flex; + justify-content: center; +} + +.swiper-button-next, +.swiper-button-prev { + background: rgba($black, .5); + width: 30px; + height: 30px; + + &:after { + font-size: 14px; + font-weight: 900; + line-height: 0; + color: white; + } +} + +.read { + overflow: hidden; + + &.more { + max-height: 80px; + transition: max-height .3s ease-in-out; + } + + &.less { + max-height: 500px; + transition: max-height .3s ease-in-out; + } +} + +.read-toggle { + text-decoration: underline; +} + +.avatar { + > svg { + width: 50px; + height: 50px; + } +} + +.loan-application { + padding: 8px 12px; + background-color: $secondary; + width: fit-content; + > p { + margin: 0; + } +} + +.comment { + display: flex; + flex-direction: column; + + > .comment-header { + display: flex; + justify-content: space-between; + + > p { + margin: 0; + } + } +} diff --git a/assets/styles/_reset.scss b/assets/styles/_reset.scss new file mode 100644 index 0000000..faacd17 --- /dev/null +++ b/assets/styles/_reset.scss @@ -0,0 +1,10 @@ +a { + text-decoration: none; + color: inherit; +} + +ul { + margin-top: 0; + padding: 0; + list-style-type: none; +} diff --git a/assets/styles/_upload.scss b/assets/styles/_upload.scss new file mode 100644 index 0000000..dd1f517 --- /dev/null +++ b/assets/styles/_upload.scss @@ -0,0 +1,15 @@ +.upload { + width: fit-content; + .upload-input { + padding: 62px; + border: 1px solid $blue-200; + background-color: $blue-100; + + } + + input[type=file] { + visibility: hidden; + width: 0; + height: 0 + } +} diff --git a/assets/styles/_utilities.scss b/assets/styles/_utilities.scss new file mode 100644 index 0000000..247a372 --- /dev/null +++ b/assets/styles/_utilities.scss @@ -0,0 +1,112 @@ +.hidden { + display: none !important; +} + +.cursor-pointer { + cursor: pointer; +} + +.rotate-180 { + transform: rotate(-180deg); +} + +.-rotate-90 { + transform: rotate(-90deg); +} + +.rotate-90 { + transform: rotate(90deg); +} + +.text-gray { + color: $gray-900; +} + +.text-blue-500 { + color: $blue-500; +} + +.fit-content { + width: fit-content; +} + +.fs-7 { + font-size: $font-size-base * .75; +} + +.lh-0 { + line-height: 0; +} + +.bg-primary-100 { + background-color: $blue-100; +} + +.border-primary-200 { + border: 1px solid $blue-200; +} + +.text-bg-blue-custom { + color: $blue-500; + border: 1px solid $blue-200; + background-color: $blue-100; +} + +.featured-image { + width: 74px; + height: 74px; + border-radius: 4px; + object-fit: cover; +} + +.user-image { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; +} +.img-avatar { + display: inline-block; + background-size: cover; + background-position: center center; + border-radius: 50% +} +.bg-secondary-subtle { + background: $gray-100; +} + +.fw-medium { + font-weight: 500; +} + +.default-user-avatar { + font-size: 40px; +} + +.fit-content { + width: fit-content; +} + +.white-space-normal{ + white-space: normal; +} + +.search-input { + > input { + border-right: 0; + } + &-button { + border: 1px solid #ced4da; + border-left: none; + background-color: $white; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: var(--bs-border-radius-sm) !important; + border-top-right-radius: var(--bs-border-radius-sm) !important; + } +} +@media screen and (max-width: 992px) { + .img-funding { + width: 100%; + } +} diff --git a/assets/styles/global.scss b/assets/styles/global.scss new file mode 100644 index 0000000..5e9c896 --- /dev/null +++ b/assets/styles/global.scss @@ -0,0 +1,46 @@ +// customize some Bootstrap variables +// $primary: darken(#428bca, 20%); // muse rebuild to see the change + +@import "custom-variables"; + +// the ~ allows you to reference things in node_modules +@import "~bootstrap/scss/bootstrap"; +@import "~bootstrap-icons/font/bootstrap-icons"; +@import "~flatpickr/dist/flatpickr.min.css"; + +@import "reset"; +@import "product-list"; +@import "card"; +@import "pagination"; +@import "product"; +@import "utilities"; +@import "my-account"; +@import "connexion"; +@import "conversations"; +@import "accordion"; +@import "calendar"; +@import "headings"; +@import "lender"; +@import "grid"; +@import "upload"; +@import "address"; +@import "chat"; +@import "group"; + +html, +body { + overflow-x: hidden; +} + +body { + // Hack to force footer in the bottom of the page + display: flex; + flex-direction: column; + min-height: 100vh; +} + +* { + margin: 0; + padding: 0; + font-family: 'Inter', sans-serif; +} diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..c933dc5 --- /dev/null +++ b/bin/console @@ -0,0 +1,17 @@ +#!/usr/bin/env php +=8.1.11", + "ext-apcu": "*", + "ext-ctype": "*", + "ext-iconv": "*", + "ext-intl": "*", + "ext-xsl": "*", + "ext-zip": "*", + "api-platform/core": "^3.1", + "craue/formflow-bundle": "^3.6", + "doctrine/annotations": "^1.0", + "doctrine/doctrine-bundle": "^2.7", + "doctrine/doctrine-migrations-bundle": "^3.2", + "doctrine/orm": "^2.13", + "easycorp/easyadmin-bundle": "v4.5.1", + "friendsofsymfony/ckeditor-bundle": "^2.4", + "geocoder-php/nominatim-provider": "^5.6", + "handcraftedinthealps/goodby-csv": "^1.4", + "hautelook/alice-bundle": "^2.11", + "knplabs/knp-paginator-bundle": "^6.1", + "league/commonmark": "^2.3", + "league/flysystem-aws-s3-v3": "^3.15", + "league/flysystem-bundle": "^3.0", + "league/flysystem-google-cloud-storage": "^3.15", + "league/flysystem-memory": "^3.10", + "meilisearch/meilisearch-php": "^1.0", + "moneyphp/money": "^3.3", + "nelmio/cors-bundle": "^2.2", + "nesbot/carbon": "^2.66", + "nyholm/psr7": "^1.0", + "odolbeau/phone-number-bundle": "^3.9", + "payum/offline": "^1.7", + "payum/payum-bundle": "^2.5", + "phpdocumentor/reflection-docblock": "^5.3", + "phpstan/phpdoc-parser": "^1.11", + "sensio/framework-extra-bundle": "^6.2", + "snc/redis-bundle": "^4.3", + "stof/doctrine-extensions-bundle": "^1.7", + "symfony/asset": "6.2.*", + "symfony/cache": "6.2.*", + "symfony/clock": "6.2.*", + "symfony/console": "6.2.*", + "symfony/doctrine-messenger": "6.2.*", + "symfony/dotenv": "6.2.*", + "symfony/expression-language": "6.2.*", + "symfony/fake-sms-notifier": "6.2.*", + "symfony/flex": "^2", + "symfony/form": "6.2.*", + "symfony/framework-bundle": "6.2.*", + "symfony/google-mailer": "6.2.*", + "symfony/html-sanitizer": "6.2.*", + "symfony/http-client": "6.2.*", + "symfony/mailer": "6.2.*", + "symfony/mercure-bundle": "^0.3.5", + "symfony/messenger": "6.2.*", + "symfony/mime": "6.2.*", + "symfony/monolog-bundle": "^3.8", + "symfony/notifier": "6.2.*", + "symfony/ovh-cloud-notifier": "6.2.*", + "symfony/property-access": "6.2.*", + "symfony/property-info": "6.2.*", + "symfony/proxy-manager-bridge": "6.2.*", + "symfony/rate-limiter": "6.2.*", + "symfony/requirements-checker": "^2.0", + "symfony/runtime": "6.2.*", + "symfony/security-bundle": "6.2.*", + "symfony/serializer": "6.2.*", + "symfony/translation-contracts": "^3.2", + "symfony/twig-bridge": "6.2.*", + "symfony/twig-bundle": "6.2.*", + "symfony/twilio-notifier": "6.2.*", + "symfony/uid": "6.2.*", + "symfony/ux-autocomplete": "^2.7", + "symfony/validator": "6.2.*", + "symfony/webpack-encore-bundle": "^1.16", + "symfony/workflow": "6.2.*", + "symfony/yaml": "6.2.*", + "twig/cssinliner-extra": "^3.4", + "twig/extra-bundle": "^3.4", + "twig/inky-extra": "^3.4", + "twig/intl-extra": "^3.5", + "twig/markdown-extra": "^3.4", + "twig/twig": "^2.12|^3.0", + "webbaard/payum-mollie": "^1.0", + "webmozart/assert": "^1.11", + "willdurand/geocoder-bundle": "^5.18" + }, + "config": { + "allow-plugins": { + "composer/package-versions-deprecated": true, + "symfony/flex": true, + "symfony/runtime": true, + "phpstan/extension-installer": true, + "php-http/discovery": true + }, + "optimize-autoloader": true, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "replace": { + "symfony/polyfill-ctype": "*", + "symfony/polyfill-iconv": "*", + "symfony/polyfill-php72": "*", + "symfony/polyfill-php73": "*", + "symfony/polyfill-php74": "*", + "symfony/polyfill-php80": "*", + "symfony/polyfill-php81": "*" + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "ckeditor:install --tag=4.22.1": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd", + "requirements-checker": "script" + }, + "post-install-cmd": [ + "@auto-scripts" + ], + "post-update-cmd": [ + "@auto-scripts" + ] + }, + "conflict": { + "symfony/symfony": "*" + }, + "extra": { + "symfony": { + "allow-contrib": false, + "require": "6.2.*", + "docker": true + } + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "friendsofphp/php-cs-fixer": "^3.12", + "friendsoftwig/twigcs": "^6.1", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-doctrine": "^1.3", + "phpstan/phpstan-strict-rules": "^1.4", + "phpstan/phpstan-symfony": "^1.2", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.14.5", + "staabm/annotate-pull-request-from-checkstyle": "^1.8", + "symfony/browser-kit": "6.2.*", + "symfony/css-selector": "6.2.*", + "symfony/debug-bundle": "6.2.*", + "symfony/maker-bundle": "^1.47", + "symfony/panther": "^2.0", + "symfony/phpunit-bridge": "^6.1", + "symfony/web-profiler-bundle": "6.2.*", + "zenstruck/messenger-test": "^1.5" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..3c77d18 --- /dev/null +++ b/composer.lock @@ -0,0 +1,18594 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "3e1f8a3631e991b528d9f68758299e0e", + "packages": [ + { + "name": "alcohol/iso4217", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/alcohol/iso4217.git", + "reference": "422890279c805e98dcea012f3dbbc184ac8afbe5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/alcohol/iso4217/zipball/422890279c805e98dcea012f3dbbc184ac8afbe5", + "reference": "422890279c805e98dcea012f3dbbc184ac8afbe5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Alcohol\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com" + } + ], + "description": "ISO 4217 PHP Library", + "homepage": "http://alcohol.github.io/iso4217/", + "keywords": [ + "4217", + "ISO 4217", + "currencies", + "iso", + "library" + ], + "support": { + "issues": "https://github.com/alcohol/iso4217/issues", + "source": "https://github.com/alcohol/iso4217" + }, + "time": "2023-02-28T12:25:39+00:00" + }, + { + "name": "api-platform/core", + "version": "v3.1.11", + "source": { + "type": "git", + "url": "https://github.com/api-platform/core.git", + "reference": "1fc611e59f7e87a89eb72ee069342916fc66c62d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/core/zipball/1fc611e59f7e87a89eb72ee069342916fc66c62d", + "reference": "1fc611e59f7e87a89eb72ee069342916fc66c62d", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^1.0 || ^2.0", + "php": ">=8.1", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^3.1", + "symfony/http-foundation": "^6.1", + "symfony/http-kernel": "^6.1", + "symfony/property-access": "^6.1", + "symfony/property-info": "^6.1", + "symfony/serializer": "^6.1", + "symfony/web-link": "^6.1", + "willdurand/negotiation": "^3.0" + }, + "conflict": { + "doctrine/common": "<3.2.2", + "doctrine/dbal": "<2.10", + "doctrine/mongodb-odm": "<2.4", + "doctrine/orm": "<2.14.0", + "doctrine/persistence": "<1.3", + "elasticsearch/elasticsearch": ">=8.0", + "phpspec/prophecy": "<1.15", + "phpunit/phpunit": "<9.5", + "symfony/service-contracts": "<3", + "symfony/var-exporter": "<6.1.1" + }, + "require-dev": { + "behat/behat": "^3.1", + "behat/mink": "^1.9@dev", + "doctrine/cache": "^1.11 || ^2.1", + "doctrine/common": "^3.2.2", + "doctrine/data-fixtures": "^1.2.2", + "doctrine/dbal": "^3.4.0", + "doctrine/doctrine-bundle": "^1.12 || ^2.0", + "doctrine/mongodb-odm": "^2.2", + "doctrine/mongodb-odm-bundle": "^4.0", + "doctrine/orm": "^2.14", + "elasticsearch/elasticsearch": "^7.11.0", + "friends-of-behat/mink-browserkit-driver": "^1.3.1", + "friends-of-behat/mink-extension": "^2.2", + "friends-of-behat/symfony-extension": "^2.1", + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "jangregor/phpstan-prophecy": "^1.0", + "justinrainbow/json-schema": "^5.2.1", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpdoc-parser": "^1.13", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-doctrine": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-symfony": "^1.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "ramsey/uuid": "^3.7 || ^4.0", + "ramsey/uuid-doctrine": "^1.4", + "soyuka/contexts": "^3.3.6", + "soyuka/stubs-mongodb": "^1.0", + "symfony/asset": "^6.1", + "symfony/browser-kit": "^6.1", + "symfony/cache": "^6.1", + "symfony/config": "^6.1", + "symfony/console": "^6.1", + "symfony/css-selector": "^6.1", + "symfony/dependency-injection": "^6.1", + "symfony/doctrine-bridge": "^6.1", + "symfony/dom-crawler": "^6.1", + "symfony/error-handler": "^6.1", + "symfony/event-dispatcher": "^6.1", + "symfony/expression-language": "^6.1", + "symfony/finder": "^6.1", + "symfony/form": "^6.1", + "symfony/framework-bundle": "^6.1", + "symfony/http-client": "^6.1", + "symfony/intl": "^6.1", + "symfony/maker-bundle": "^1.24", + "symfony/mercure-bundle": "*", + "symfony/messenger": "^6.1", + "symfony/phpunit-bridge": "^6.1", + "symfony/routing": "^6.1", + "symfony/security-bundle": "^6.1", + "symfony/security-core": "^6.1", + "symfony/twig-bundle": "^6.1", + "symfony/uid": "^6.1", + "symfony/validator": "^6.1", + "symfony/web-profiler-bundle": "^6.1", + "symfony/yaml": "^6.1", + "twig/twig": "^1.42.3 || ^2.12 || ^3.0", + "webonyx/graphql-php": "^14.0 || ^15.0" + }, + "suggest": { + "doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.", + "elasticsearch/elasticsearch": "To support Elasticsearch.", + "ocramius/package-versions": "To display the API Platform's version in the debug bar.", + "phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.", + "psr/cache-implementation": "To use metadata caching.", + "ramsey/uuid": "To support Ramsey's UUID identifiers.", + "symfony/cache": "To have metadata caching when using Symfony integration.", + "symfony/config": "To load XML configuration files.", + "symfony/expression-language": "To use authorization features.", + "symfony/http-client": "To use the HTTP cache invalidation system.", + "symfony/messenger": "To support messenger integration.", + "symfony/security": "To use authorization features.", + "symfony/twig-bundle": "To use the Swagger UI integration.", + "symfony/uid": "To support Symfony UUID/ULID identifiers.", + "symfony/web-profiler-bundle": "To use the data collector.", + "webonyx/graphql-php": "To support GraphQL." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2.x-dev" + }, + "symfony": { + "require": "^6.1" + } + }, + "autoload": { + "psr-4": { + "ApiPlatform\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + } + ], + "description": "Build a fully-featured hypermedia or GraphQL API in minutes!", + "homepage": "https://api-platform.com", + "keywords": [ + "Hydra", + "JSON-LD", + "api", + "graphql", + "hal", + "jsonapi", + "openapi", + "rest", + "swagger" + ], + "support": { + "issues": "https://github.com/api-platform/core/issues", + "source": "https://github.com/api-platform/core/tree/v3.1.11" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/api-platform/core", + "type": "tidelift" + } + ], + "time": "2023-05-05T14:18:25+00:00" + }, + { + "name": "aws/aws-crt-php", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/awslabs/aws-crt-php.git", + "reference": "1926277fc71d253dfa820271ac5987bdb193ccf5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/1926277fc71d253dfa820271ac5987bdb193ccf5", + "reference": "1926277fc71d253dfa820271ac5987bdb193ccf5", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "https://github.com/awslabs/aws-crt-php", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" + ], + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.1" + }, + "time": "2023-03-24T20:22:19+00:00" + }, + { + "name": "aws/aws-sdk-php", + "version": "3.270.1", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "efc5a281bcdfb49bc1c608bb1f892403631da077" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/efc5a281bcdfb49bc1c608bb1f892403631da077", + "reference": "efc5a281bcdfb49bc1c608bb1f892403631da077", + "shasum": "" + }, + "require": { + "aws/aws-crt-php": "^1.0.4", + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "guzzlehttp/promises": "^1.4.0", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "mtdowling/jmespath.php": "^2.6", + "php": ">=5.5", + "psr/http-message": "^1.0" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "composer/composer": "^1.10.22", + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "nette/neon": "^2.3", + "paragonie/random_compat": ">= 2", + "phpunit/phpunit": "^4.8.35 || ^5.6.3 || ^9.5", + "psr/cache": "^1.0", + "psr/simple-cache": "^1.0", + "sebastian/comparator": "^1.2.3 || ^4.0", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Aws\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "support": { + "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", + "issues": "https://github.com/aws/aws-sdk-php/issues", + "source": "https://github.com/aws/aws-sdk-php/tree/3.270.1" + }, + "time": "2023-05-22T18:23:05+00:00" + }, + { + "name": "behat/transliterator", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Transliterator.git", + "reference": "baac5873bac3749887d28ab68e2f74db3a4408af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Transliterator/zipball/baac5873bac3749887d28ab68e2f74db3a4408af", + "reference": "baac5873bac3749887d28ab68e2f74db3a4408af", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "chuyskywalker/rolling-curl": "^3.1", + "php-yaoi/php-yaoi": "^1.0", + "phpunit/phpunit": "^8.5.25 || ^9.5.19" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Behat\\Transliterator\\": "src/Behat/Transliterator" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Artistic-1.0" + ], + "description": "String transliterator", + "keywords": [ + "i18n", + "slug", + "transliterator" + ], + "support": { + "issues": "https://github.com/Behat/Transliterator/issues", + "source": "https://github.com/Behat/Transliterator/tree/v1.5.0" + }, + "time": "2022-03-30T09:27:43+00:00" + }, + { + "name": "brick/math", + "version": "0.11.0", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/0ad82ce168c82ba30d1c01ec86116ab52f589478", + "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^9.0", + "vimeo/psalm": "5.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "brick", + "math" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.11.0" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2023-01-15T23:15:59+00:00" + }, + { + "name": "clue/stream-filter", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/clue/stream-filter.git", + "reference": "d6169430c7731d8509da7aecd0af756a5747b78e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/stream-filter/zipball/d6169430c7731d8509da7aecd0af756a5747b78e", + "reference": "d6169430c7731d8509da7aecd0af756a5747b78e", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "Clue\\StreamFilter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "A simple and modern approach to stream filtering in PHP", + "homepage": "https://github.com/clue/php-stream-filter", + "keywords": [ + "bucket brigade", + "callback", + "filter", + "php_user_filter", + "stream", + "stream_filter_append", + "stream_filter_register" + ], + "support": { + "issues": "https://github.com/clue/stream-filter/issues", + "source": "https://github.com/clue/stream-filter/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-02-21T13:15:14+00:00" + }, + { + "name": "composer/ca-bundle", + "version": "1.3.5", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "74780ccf8c19d6acb8d65c5f39cd72110e132bbd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/74780ccf8c19d6acb8d65c5f39cd72110e132bbd", + "reference": "74780ccf8c19d6acb8d65c5f39cd72110e132bbd", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.55", + "psr/log": "^1.0", + "symfony/phpunit-bridge": "^4.2 || ^5", + "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.3.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-01-11T08:27:00+00:00" + }, + { + "name": "craue/formflow-bundle", + "version": "3.6.0", + "source": { + "type": "git", + "url": "https://github.com/craue/CraueFormFlowBundle.git", + "reference": "af3db3519a9e62920c439b80d17ff4db689a5d60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/craue/CraueFormFlowBundle/zipball/af3db3519a9e62920c439b80d17ff4db689a5d60", + "reference": "af3db3519a9e62920c439b80d17ff4db689a5d60", + "shasum": "" + }, + "require": { + "php": "^7.3|^8", + "symfony/config": "~4.4|~5.3|^6.0", + "symfony/dependency-injection": "~4.4|~5.3|^6.0", + "symfony/event-dispatcher": "~4.4|~5.3|^6.0", + "symfony/form": "~4.4|~5.3|^6.0", + "symfony/http-foundation": "~4.4|~5.3|^6.0", + "symfony/http-kernel": "~4.4|~5.3|^6.0", + "symfony/options-resolver": "~4.4|~5.3|^6.0", + "symfony/security-core": "~4.4|~5.3|^6.0", + "symfony/translation": "~4.4|~5.3|^6.0", + "symfony/validator": "~4.4|~5.3|^6.0", + "symfony/yaml": "~4.4|~5.3|^6.0" + }, + "require-dev": { + "craue/translations-tests": "^1.1", + "doctrine/annotations": "^1.6", + "doctrine/common": "~2.9|~3.0", + "doctrine/doctrine-bundle": "~1.10|~2.0", + "phpunit/phpunit": "^9.5", + "symfony/browser-kit": "~4.4|~5.3|^6.0", + "symfony/css-selector": "~4.4|~5.3|^6.0", + "symfony/mime": "~4.4|~5.3|^6.0", + "symfony/phpunit-bridge": "^6", + "symfony/security-bundle": "~4.4|~5.3|^6.0", + "symfony/twig-bundle": "~4.4|~5.3|^6.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.6.x-dev" + }, + "symfony": { + "require": "~4.4|~5.3|^6.0" + } + }, + "autoload": { + "psr-4": { + "Craue\\FormFlowBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Raue", + "email": "christian.raue@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/craue/CraueFormFlowBundle/contributors" + } + ], + "description": "Multi-step forms for your Symfony project.", + "homepage": "https://github.com/craue/CraueFormFlowBundle", + "keywords": [ + "form", + "step", + "symfony", + "wizard" + ], + "support": { + "issues": "https://github.com/craue/CraueFormFlowBundle/issues", + "source": "https://github.com/craue/CraueFormFlowBundle/tree/3.6.0" + }, + "time": "2022-01-24T14:43:05+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "f41715465d65213d644d3141a6a93081be5d3549" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/f41715465d65213d644d3141a6a93081be5d3549", + "reference": "f41715465d65213d644d3141a6a93081be5d3549", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.2" + }, + "time": "2022-10-27T11:44:00+00:00" + }, + { + "name": "doctrine/annotations", + "version": "1.14.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "fb0d71a7393298a7b232cbf4c8b1f73f3ec3d5af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/fb0d71a7393298a7b232cbf4c8b1f73f3ec3d5af", + "reference": "fb0d71a7393298a7b232cbf4c8b1f73f3ec3d5af", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1 || ^2", + "ext-tokenizer": "*", + "php": "^7.1 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/coding-standard": "^9 || ^10", + "phpstan/phpstan": "~1.4.10 || ^1.8.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "symfony/cache": "^4.4 || ^5.4 || ^6", + "vimeo/psalm": "^4.10" + }, + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/1.14.3" + }, + "time": "2023-02-01T09:20:38+00:00" + }, + { + "name": "doctrine/cache", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", + "shasum": "" + }, + "require": { + "php": "~7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/coding-standard": "^9", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "symfony/cache": "^4.4 || ^5.4 || ^6", + "symfony/var-exporter": "^4.4 || ^5.4 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", + "keywords": [ + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" + ], + "support": { + "issues": "https://github.com/doctrine/cache/issues", + "source": "https://github.com/doctrine/cache/tree/2.2.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], + "time": "2022-05-20T20:07:39+00:00" + }, + { + "name": "doctrine/collections", + "version": "2.1.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "db8cda536a034337f7dd63febecc713d4957f9ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/db8cda536a034337f7dd63febecc713d4957f9ee", + "reference": "db8cda536a034337f7dd63febecc713d4957f9ee", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1", + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^10.0", + "ext-json": "*", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.22" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Collections\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", + "homepage": "https://www.doctrine-project.org/projects/collections.html", + "keywords": [ + "array", + "collections", + "iterators", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/collections/issues", + "source": "https://github.com/doctrine/collections/tree/2.1.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcollections", + "type": "tidelift" + } + ], + "time": "2022-12-27T23:41:38+00:00" + }, + { + "name": "doctrine/common", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/common.git", + "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/common/zipball/8b5e5650391f851ed58910b3e3d48a71062eeced", + "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced", + "shasum": "" + }, + "require": { + "doctrine/persistence": "^2.0 || ^3.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0 || ^10.0", + "doctrine/collections": "^1", + "phpstan/phpstan": "^1.4.1", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5.20 || ^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.0", + "symfony/phpunit-bridge": "^6.1", + "vimeo/psalm": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, proxies and much more.", + "homepage": "https://www.doctrine-project.org/projects/common.html", + "keywords": [ + "common", + "doctrine", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/common/issues", + "source": "https://github.com/doctrine/common/tree/3.4.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcommon", + "type": "tidelift" + } + ], + "time": "2022-10-09T11:47:59+00:00" + }, + { + "name": "doctrine/data-fixtures", + "version": "1.6.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/data-fixtures.git", + "reference": "4af35dadbfcf4b00abb2a217c4c8c8800cf5fcf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/4af35dadbfcf4b00abb2a217c4c8c8800cf5fcf4", + "reference": "4af35dadbfcf4b00abb2a217c4c8c8800cf5fcf4", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^0.5.3 || ^1.0", + "doctrine/persistence": "^1.3.3 || ^2.0 || ^3.0", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "doctrine/dbal": "<2.13", + "doctrine/orm": "<2.12", + "doctrine/phpcr-odm": "<1.3.0" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "doctrine/dbal": "^2.13 || ^3.0", + "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0", + "doctrine/orm": "^2.12", + "ext-sqlite3": "*", + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^8.5 || ^9.5 || ^10.0", + "symfony/cache": "^5.0 || ^6.0", + "vimeo/psalm": "^4.10 || ^5.9" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)", + "doctrine/mongodb-odm": "For loading MongoDB ODM fixtures", + "doctrine/orm": "For loading ORM fixtures", + "doctrine/phpcr-odm": "For loading PHPCR ODM fixtures" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\DataFixtures\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Data Fixtures for all Doctrine Object Managers", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "database" + ], + "support": { + "issues": "https://github.com/doctrine/data-fixtures/issues", + "source": "https://github.com/doctrine/data-fixtures/tree/1.6.6" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdata-fixtures", + "type": "tidelift" + } + ], + "time": "2023-04-20T13:08:54+00:00" + }, + { + "name": "doctrine/dbal", + "version": "3.6.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "b4bd1cfbd2b916951696d82e57d054394d84864c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/b4bd1cfbd2b916951696d82e57d054394d84864c", + "reference": "b4bd1cfbd2b916951696d82e57d054394d84864c", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/cache": "^1.11|^2.0", + "doctrine/deprecations": "^0.5.3|^1", + "doctrine/event-manager": "^1|^2", + "php": "^7.4 || ^8.0", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "11.1.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2022.3", + "phpstan/phpstan": "1.10.9", + "phpstan/phpstan-strict-rules": "^1.5", + "phpunit/phpunit": "9.6.6", + "psalm/plugin-phpunit": "0.18.4", + "squizlabs/php_codesniffer": "3.7.2", + "symfony/cache": "^5.4|^6.0", + "symfony/console": "^4.4|^5.4|^6.0", + "vimeo/psalm": "4.30.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/3.6.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2023-04-14T07:25:38+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", + "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", + "shasum": "" + }, + "require": { + "php": "^7.1|^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpunit/phpunit": "^7.5|^8.5|^9.5", + "psr/log": "^1|^2|^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/v1.0.0" + }, + "time": "2022-05-02T15:47:09+00:00" + }, + { + "name": "doctrine/doctrine-bundle", + "version": "2.9.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineBundle.git", + "reference": "7539b3c8bd620f7df6c2c6d510204bd2ce0064e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/7539b3c8bd620f7df6c2c6d510204bd2ce0064e3", + "reference": "7539b3c8bd620f7df6c2c6d510204bd2ce0064e3", + "shasum": "" + }, + "require": { + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/dbal": "^3.6.0", + "doctrine/persistence": "^2.2 || ^3", + "doctrine/sql-formatter": "^1.0.1", + "php": "^7.4 || ^8.0", + "symfony/cache": "^5.4 || ^6.0", + "symfony/config": "^5.4 || ^6.0", + "symfony/console": "^5.4 || ^6.0", + "symfony/dependency-injection": "^5.4 || ^6.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/doctrine-bridge": "^5.4.19 || ^6.0.7", + "symfony/framework-bundle": "^5.4 || ^6.0", + "symfony/service-contracts": "^1.1.1 || ^2.0 || ^3" + }, + "conflict": { + "doctrine/annotations": ">=3.0", + "doctrine/orm": "<2.11 || >=3.0", + "twig/twig": "<1.34 || >=2.0 <2.4" + }, + "require-dev": { + "doctrine/annotations": "^1 || ^2", + "doctrine/coding-standard": "^9.0", + "doctrine/deprecations": "^1.0", + "doctrine/orm": "^2.11 || ^3.0", + "friendsofphp/proxy-manager-lts": "^1.0", + "phpunit/phpunit": "^9.5.26 || ^10.0", + "psalm/plugin-phpunit": "^0.18.4", + "psalm/plugin-symfony": "^4", + "psr/log": "^1.1.4 || ^2.0 || ^3.0", + "symfony/phpunit-bridge": "^6.1", + "symfony/property-info": "^5.4 || ^6.0", + "symfony/proxy-manager-bridge": "^5.4 || ^6.0", + "symfony/security-bundle": "^5.4 || ^6.0", + "symfony/twig-bridge": "^5.4 || ^6.0", + "symfony/validator": "^5.4 || ^6.0", + "symfony/web-profiler-bundle": "^5.4 || ^6.0", + "symfony/yaml": "^5.4 || ^6.0", + "twig/twig": "^1.34 || ^2.12 || ^3.0", + "vimeo/psalm": "^4.30" + }, + "suggest": { + "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", + "ext-pdo": "*", + "symfony/web-profiler-bundle": "To use the data collector." + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\DoctrineBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org/" + } + ], + "description": "Symfony DoctrineBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "database", + "dbal", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineBundle/issues", + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.9.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-bundle", + "type": "tidelift" + } + ], + "time": "2023-04-14T05:39:34+00:00" + }, + { + "name": "doctrine/doctrine-migrations-bundle", + "version": "3.2.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", + "reference": "3393f411ba25ade21969c33f2053220044854d01" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/3393f411ba25ade21969c33f2053220044854d01", + "reference": "3393f411ba25ade21969c33f2053220044854d01", + "shasum": "" + }, + "require": { + "doctrine/doctrine-bundle": "~1.0|~2.0", + "doctrine/migrations": "^3.2", + "php": "^7.2|^8.0", + "symfony/framework-bundle": "~3.4|~4.0|~5.0|~6.0" + }, + "require-dev": { + "doctrine/coding-standard": "^8.0", + "doctrine/orm": "^2.6", + "doctrine/persistence": "^1.3||^2.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-deprecation-rules": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/phpunit": "^8.0|^9.0", + "vimeo/psalm": "^4.11" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\MigrationsBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DoctrineMigrationsBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "dbal", + "migrations", + "schema" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", + "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.2.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-migrations-bundle", + "type": "tidelift" + } + ], + "time": "2022-02-01T18:08:07+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "750671534e0241a7c50ea5b43f67e23eb5c96f32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/750671534e0241a7c50ea5b43f67e23eb5c96f32", + "reference": "750671534e0241a7c50ea5b43f67e23eb5c96f32", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^10", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2022-10-12T20:59:15+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.0.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "d9d313a36c872fd6ee06d9a6cbcf713eaa40f024" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/d9d313a36c872fd6ee06d9a6cbcf713eaa40f024", + "reference": "d9d313a36c872fd6ee06d9a6cbcf713eaa40f024", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^10", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.6" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2022-10-20T09:10:12+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "doctrine/lexer", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", + "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^10", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^4.11 || ^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2022-12-14T08:49:07+00:00" + }, + { + "name": "doctrine/migrations", + "version": "3.6.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/migrations.git", + "reference": "e542ad8bcd606d7a18d0875babb8a6d963c9c059" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/e542ad8bcd606d7a18d0875babb8a6d963c9c059", + "reference": "e542ad8bcd606d7a18d0875babb8a6d963c9c059", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/dbal": "^3.5.1", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.2 || ^2.0", + "php": "^8.1", + "psr/log": "^1.1.3 || ^2 || ^3", + "symfony/console": "^4.4.16 || ^5.4 || ^6.0", + "symfony/stopwatch": "^4.4 || ^5.4 || ^6.0", + "symfony/var-exporter": "^6.2" + }, + "conflict": { + "doctrine/orm": "<2.12" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "doctrine/orm": "^2.13", + "doctrine/persistence": "^2 || ^3", + "doctrine/sql-formatter": "^1.0", + "ext-pdo_sqlite": "*", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.1", + "phpstan/phpstan-symfony": "^1.1", + "phpunit/phpunit": "^9.5.24", + "symfony/cache": "^4.4 || ^5.4 || ^6.0", + "symfony/process": "^4.4 || ^5.4 || ^6.0", + "symfony/yaml": "^4.4 || ^5.4 || ^6.0" + }, + "suggest": { + "doctrine/sql-formatter": "Allows to generate formatted SQL with the diff command.", + "symfony/yaml": "Allows the use of yaml for migration configuration files." + }, + "bin": [ + "bin/doctrine-migrations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Migrations\\": "lib/Doctrine/Migrations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Michael Simonson", + "email": "contact@mikesimonson.com" + } + ], + "description": "PHP Doctrine Migrations project offer additional functionality on top of the database abstraction layer (DBAL) for versioning your database schema and easily deploying changes to it. It is a very easy to use and a powerful tool.", + "homepage": "https://www.doctrine-project.org/projects/migrations.html", + "keywords": [ + "database", + "dbal", + "migrations" + ], + "support": { + "issues": "https://github.com/doctrine/migrations/issues", + "source": "https://github.com/doctrine/migrations/tree/3.6.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fmigrations", + "type": "tidelift" + } + ], + "time": "2023-02-15T18:49:46+00:00" + }, + { + "name": "doctrine/orm", + "version": "2.15.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/orm.git", + "reference": "9bc6f5b4ac6f1e7d4248b2efbd01a748782075bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/orm/zipball/9bc6f5b4ac6f1e7d4248b2efbd01a748782075bc", + "reference": "9bc6f5b4ac6f1e7d4248b2efbd01a748782075bc", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/cache": "^1.12.1 || ^2.1.1", + "doctrine/collections": "^1.5 || ^2.1", + "doctrine/common": "^3.0.3", + "doctrine/dbal": "^2.13.1 || ^3.2", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.2 || ^2", + "doctrine/inflector": "^1.4 || ^2.0", + "doctrine/instantiator": "^1.3 || ^2", + "doctrine/lexer": "^2", + "doctrine/persistence": "^2.4 || ^3", + "ext-ctype": "*", + "php": "^7.1 || ^8.0", + "psr/cache": "^1 || ^2 || ^3", + "symfony/console": "^4.2 || ^5.0 || ^6.0", + "symfony/polyfill-php72": "^1.23", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "doctrine/annotations": "<1.13 || >= 3.0" + }, + "require-dev": { + "doctrine/annotations": "^1.13 || ^2", + "doctrine/coding-standard": "^9.0.2 || ^12.0", + "phpbench/phpbench": "^0.16.10 || ^1.0", + "phpstan/phpstan": "~1.4.10 || 1.10.14", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", + "psr/log": "^1 || ^2 || ^3", + "squizlabs/php_codesniffer": "3.7.2", + "symfony/cache": "^4.4 || ^5.4 || ^6.0", + "symfony/var-exporter": "^4.4 || ^5.4 || ^6.2", + "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0", + "vimeo/psalm": "4.30.0 || 5.11.0" + }, + "suggest": { + "ext-dom": "Provides support for XSD validation for XML mapping files", + "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0", + "symfony/yaml": "If you want to use YAML Metadata Mapping Driver" + }, + "bin": [ + "bin/doctrine" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\ORM\\": "lib/Doctrine/ORM" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Object-Relational-Mapper for PHP", + "homepage": "https://www.doctrine-project.org/projects/orm.html", + "keywords": [ + "database", + "orm" + ], + "support": { + "issues": "https://github.com/doctrine/orm/issues", + "source": "https://github.com/doctrine/orm/tree/2.15.1" + }, + "time": "2023-05-07T18:56:25+00:00" + }, + { + "name": "doctrine/persistence", + "version": "3.1.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/persistence.git", + "reference": "8bf8ab15960787f1a49d405f6eb8c787b4841119" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/8bf8ab15960787f1a49d405f6eb8c787b4841119", + "reference": "8bf8ab15960787f1a49d405f6eb8c787b4841119", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^1 || ^2", + "php": "^7.2 || ^8.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0" + }, + "conflict": { + "doctrine/common": "<2.10" + }, + "require-dev": { + "composer/package-versions-deprecated": "^1.11", + "doctrine/coding-standard": "^11", + "doctrine/common": "^3.0", + "phpstan/phpstan": "1.9.4", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.5", + "symfony/cache": "^4.4 || ^5.4 || ^6.0", + "vimeo/psalm": "4.30.0 || 5.3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Persistence\\": "src/Persistence" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.", + "homepage": "https://www.doctrine-project.org/projects/persistence.html", + "keywords": [ + "mapper", + "object", + "odm", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/persistence/issues", + "source": "https://github.com/doctrine/persistence/tree/3.1.4" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fpersistence", + "type": "tidelift" + } + ], + "time": "2023-02-03T11:13:07+00:00" + }, + { + "name": "doctrine/sql-formatter", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/sql-formatter.git", + "reference": "25a06c7bf4c6b8218f47928654252863ffc890a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/25a06c7bf4c6b8218f47928654252863ffc890a5", + "reference": "25a06c7bf4c6b8218f47928654252863ffc890a5", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4" + }, + "bin": [ + "bin/sql-formatter" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\SqlFormatter\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeremy Dorn", + "email": "jeremy@jeremydorn.com", + "homepage": "https://jeremydorn.com/" + } + ], + "description": "a PHP SQL highlighting library", + "homepage": "https://github.com/doctrine/sql-formatter/", + "keywords": [ + "highlight", + "sql" + ], + "support": { + "issues": "https://github.com/doctrine/sql-formatter/issues", + "source": "https://github.com/doctrine/sql-formatter/tree/1.1.3" + }, + "time": "2022-05-23T21:33:49+00:00" + }, + { + "name": "easycorp/easyadmin-bundle", + "version": "v4.5.1", + "source": { + "type": "git", + "url": "https://github.com/EasyCorp/EasyAdminBundle.git", + "reference": "9c00e8128b99000e8e400315abe740f864d3e6e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/EasyCorp/EasyAdminBundle/zipball/9c00e8128b99000e8e400315abe740f864d3e6e4", + "reference": "9c00e8128b99000e8e400315abe740f864d3e6e4", + "shasum": "" + }, + "require": { + "doctrine/doctrine-bundle": "^2.5", + "doctrine/orm": "^2.10", + "ext-json": "*", + "php": ">=8.0.2", + "symfony/asset": "^5.4|^6.0", + "symfony/cache": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/deprecation-contracts": "^3.0", + "symfony/doctrine-bridge": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/form": "^5.4|^6.0", + "symfony/framework-bundle": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/intl": "^5.4|^6.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/security-bundle": "^5.4|^6.0", + "symfony/string": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/twig-bundle": "^5.4|^6.0", + "symfony/uid": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0" + }, + "require-dev": { + "doctrine/doctrine-fixtures-bundle": "^3.4", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-phpunit": "^1.2", + "phpstan/phpstan-strict-rules": "^1.4", + "phpstan/phpstan-symfony": "^1.2", + "psr/log": "^1.0", + "symfony/browser-kit": "^5.4|^6.0", + "symfony/css-selector": "^5.4|^6.0", + "symfony/dom-crawler": "^5.4|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "EasyCorp\\Bundle\\EasyAdminBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Project Contributors", + "homepage": "https://github.com/EasyCorp/EasyAdminBundle/graphs/contributors" + } + ], + "description": "Admin generator for Symfony applications", + "homepage": "https://github.com/EasyCorp/EasyAdminBundle", + "keywords": [ + "admin", + "backend", + "generator" + ], + "support": { + "issues": "https://github.com/EasyCorp/EasyAdminBundle/issues", + "source": "https://github.com/EasyCorp/EasyAdminBundle/tree/v4.5.1" + }, + "funding": [ + { + "url": "https://github.com/javiereguiluz", + "type": "github" + } + ], + "time": "2023-01-19T19:18:11+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "3a85486b709bc384dae8eb78fb2eec649bdb64ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/3a85486b709bc384dae8eb78fb2eec649bdb64ff", + "reference": "3a85486b709bc384dae8eb78fb2eec649bdb64ff", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^4.30" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2023-01-14T14:17:03+00:00" + }, + { + "name": "fakerphp/faker", + "version": "v1.22.0", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "f85772abd508bd04e20bb4b1bbe260a68d0066d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/f85772abd508bd04e20bb4b1bbe260a68d0066d2", + "reference": "f85772abd508bd04e20bb4b1bbe260a68d0066d2", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "v1.21-dev" + } + }, + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.22.0" + }, + "time": "2023-05-14T12:31:37+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v6.5.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "e94e7353302b0c11ec3cfff7180cd0b1743975d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/e94e7353302b0c11ec3cfff7180cd0b1743975d2", + "reference": "e94e7353302b0c11ec3cfff7180cd0b1743975d2", + "shasum": "" + }, + "require": { + "php": "^7.4||^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^6.5||^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^1.0||^2.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.5.0" + }, + "time": "2023-05-12T15:47:07+00:00" + }, + { + "name": "friendsofphp/proxy-manager-lts", + "version": "v1.0.15", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/proxy-manager-lts.git", + "reference": "3ca65b8edb8d96b46fedc9bb4911396516347129" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/proxy-manager-lts/zipball/3ca65b8edb8d96b46fedc9bb4911396516347129", + "reference": "3ca65b8edb8d96b46fedc9bb4911396516347129", + "shasum": "" + }, + "require": { + "laminas/laminas-code": "~3.4.1|^4.0", + "php": ">=7.1", + "symfony/filesystem": "^4.4.17|^5.0|^6.0" + }, + "conflict": { + "laminas/laminas-stdlib": "<3.2.1", + "zendframework/zend-stdlib": "<3.2.1" + }, + "replace": { + "ocramius/proxy-manager": "^2.1" + }, + "require-dev": { + "ext-phar": "*", + "symfony/phpunit-bridge": "^5.4|^6.0" + }, + "type": "library", + "extra": { + "thanks": { + "name": "ocramius/proxy-manager", + "url": "https://github.com/Ocramius/ProxyManager" + } + }, + "autoload": { + "psr-4": { + "ProxyManager\\": "src/ProxyManager" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + } + ], + "description": "Adding support for a wider range of PHP versions to ocramius/proxy-manager", + "homepage": "https://github.com/FriendsOfPHP/proxy-manager-lts", + "keywords": [ + "aop", + "lazy loading", + "proxy", + "proxy pattern", + "service proxies" + ], + "support": { + "issues": "https://github.com/FriendsOfPHP/proxy-manager-lts/issues", + "source": "https://github.com/FriendsOfPHP/proxy-manager-lts/tree/v1.0.15" + }, + "funding": [ + { + "url": "https://github.com/Ocramius", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ocramius/proxy-manager", + "type": "tidelift" + } + ], + "time": "2023-05-16T13:18:04+00:00" + }, + { + "name": "friendsofsymfony/ckeditor-bundle", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfSymfony/FOSCKEditorBundle.git", + "reference": "a4df4056b2832881e17b939e83f25406a6cc6414" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfSymfony/FOSCKEditorBundle/zipball/a4df4056b2832881e17b939e83f25406a6cc6414", + "reference": "a4df4056b2832881e17b939e83f25406a6cc6414", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-zip": "*", + "php": "^7.1|^8.0", + "symfony/asset": "^4.4 || ^5.0 || ^6.0", + "symfony/config": "^4.4 || ^5.0 || ^6.0", + "symfony/dependency-injection": "^4.4 || ^5.0 || ^6.0", + "symfony/expression-language": "^4.4 || ^5.0 || ^6.0", + "symfony/form": "^4.4 || ^5.0 || ^6.0", + "symfony/framework-bundle": "^4.4 || ^5.0 || ^6.0", + "symfony/http-foundation": "^4.4 || ^5.0 || ^6.0", + "symfony/http-kernel": "^4.4 || ^5.0 || ^6.0", + "symfony/options-resolver": "^4.4 || ^5.0 || ^6.0", + "symfony/property-access": "^4.4 || ^5.0 || ^6.0", + "symfony/routing": "^4.4 || ^5.0 || ^6.0", + "symfony/twig-bundle": "^4.4 || ^5.0 || ^6.0", + "twig/twig": "^2.4 || ^3.0" + }, + "conflict": { + "sebastian/environment": "<1.3.4", + "sebastian/exporter": "<2.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.0", + "matthiasnoback/symfony-dependency-injection-test": "^3.0 || ^4.0", + "symfony/console": "^4.4 || ^5.0 || ^6.0", + "symfony/phpunit-bridge": "^4.4 || ^5.0 || ^6.0", + "symfony/yaml": "^4.4 || ^5.0 || ^6.0" + }, + "suggest": { + "egeloen/form-extra-bundle": "Allows to load CKEditor asynchronously" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "FOS\\CKEditorBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "FriendsOfSymfony Community", + "homepage": "https://github.com/FriendsOfSymfony/FOSCKEditorBundle/graphs/contributors" + } + ], + "description": "Provides a CKEditor integration for your Symfony project.", + "keywords": [ + "CKEditor" + ], + "support": { + "issues": "https://github.com/FriendsOfSymfony/FOSCKEditorBundle/issues", + "source": "https://github.com/FriendsOfSymfony/FOSCKEditorBundle/tree/2.4.0" + }, + "time": "2022-01-07T12:34:18+00:00" + }, + { + "name": "gedmo/doctrine-extensions", + "version": "v3.11.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine-extensions/DoctrineExtensions.git", + "reference": "ae4bdf0d567e06b6bb1902a560ee78961b230953" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/ae4bdf0d567e06b6bb1902a560ee78961b230953", + "reference": "ae4bdf0d567e06b6bb1902a560ee78961b230953", + "shasum": "" + }, + "require": { + "behat/transliterator": "~1.2", + "doctrine/annotations": "^1.13 || ^2.0", + "doctrine/collections": "^1.2 || ^2.0", + "doctrine/common": "^2.13 || ^3.0", + "doctrine/event-manager": "^1.2 || ^2.0", + "doctrine/persistence": "^2.2 || ^3.0", + "php": "^7.2 || ^8.0", + "psr/cache": "^1 || ^2 || ^3", + "symfony/cache": "^4.4 || ^5.3 || ^6.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0" + }, + "conflict": { + "doctrine/cache": "<1.11", + "doctrine/dbal": "<2.13.1 || ^3.0 <3.2", + "doctrine/mongodb-odm": "<2.3", + "doctrine/orm": "<2.10.2", + "sebastian/comparator": "<2.0" + }, + "require-dev": { + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/dbal": "^2.13.1 || ^3.2", + "doctrine/doctrine-bundle": "^2.3", + "doctrine/mongodb-odm": "^2.3", + "doctrine/orm": "^2.10.2", + "friendsofphp/php-cs-fixer": "^3.4.0,<3.10", + "nesbot/carbon": "^2.55", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-doctrine": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.5", + "symfony/console": "^4.4 || ^5.3 || ^6.0", + "symfony/phpunit-bridge": "^6.0", + "symfony/yaml": "^4.4 || ^5.3 || ^6.0" + }, + "suggest": { + "doctrine/mongodb-odm": "to use the extensions with the MongoDB ODM", + "doctrine/orm": "to use the extensions with the ORM", + "symfony/cache": "to cache parsed annotations" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.12-dev" + } + }, + "autoload": { + "psr-4": { + "Gedmo\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gediminas Morkevicius", + "email": "gediminas.morkevicius@gmail.com" + }, + { + "name": "Gustavo Falco", + "email": "comfortablynumb84@gmail.com" + }, + { + "name": "David Buchmann", + "email": "david@liip.ch" + } + ], + "description": "Doctrine behavioral extensions", + "homepage": "http://gediminasm.org/", + "keywords": [ + "Blameable", + "behaviors", + "doctrine", + "extensions", + "gedmo", + "loggable", + "nestedset", + "odm", + "orm", + "sluggable", + "sortable", + "timestampable", + "translatable", + "tree", + "uploadable" + ], + "support": { + "email": "gediminas.morkevicius@gmail.com", + "issues": "https://github.com/doctrine-extensions/DoctrineExtensions/issues", + "source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.11.1", + "wiki": "https://github.com/Atlantic18/DoctrineExtensions/tree/main/doc" + }, + "time": "2023-02-20T19:24:07+00:00" + }, + { + "name": "geocoder-php/common-http", + "version": "4.5.0", + "source": { + "type": "git", + "url": "https://github.com/geocoder-php/php-common-http.git", + "reference": "4ee2cee60d21631e2a09c196bf6b9fd296bca728" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/geocoder-php/php-common-http/zipball/4ee2cee60d21631e2a09c196bf6b9fd296bca728", + "reference": "4ee2cee60d21631e2a09c196bf6b9fd296bca728", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "php-http/client-implementation": "^1.0", + "php-http/discovery": "^1.6", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0.2", + "psr/http-message": "^1.0", + "psr/http-message-implementation": "^1.0", + "willdurand/geocoder": "^4.0" + }, + "require-dev": { + "nyholm/psr7": "^1.0", + "php-http/message": "^1.0", + "php-http/mock-client": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/stopwatch": "~2.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "Geocoder\\Http\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "Common files for HTTP based Geocoders", + "homepage": "http://geocoder-php.org", + "keywords": [ + "http geocoder" + ], + "support": { + "source": "https://github.com/geocoder-php/php-common-http/tree/4.5.0" + }, + "time": "2022-07-30T12:09:30+00:00" + }, + { + "name": "geocoder-php/nominatim-provider", + "version": "5.7.0", + "source": { + "type": "git", + "url": "https://github.com/geocoder-php/nominatim-provider.git", + "reference": "a50486161f6babad7b1ed7ee7bf86147b0844413" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/geocoder-php/nominatim-provider/zipball/a50486161f6babad7b1ed7ee7bf86147b0844413", + "reference": "a50486161f6babad7b1ed7ee7bf86147b0844413", + "shasum": "" + }, + "require": { + "geocoder-php/common-http": "^4.1", + "php": "^7.4 || ^8.0", + "willdurand/geocoder": "^4.0" + }, + "provide": { + "geocoder-php/provider-implementation": "1.0" + }, + "require-dev": { + "geocoder-php/provider-integration-tests": "^1.0", + "php-http/curl-client": "^2.2", + "php-http/message": "^1.0", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "Geocoder\\Provider\\Nominatim\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "William Durand", + "email": "william.durand1@gmail.com" + } + ], + "description": "Geocoder Nominatim adapter", + "homepage": "http://geocoder-php.org/Geocoder/", + "support": { + "source": "https://github.com/geocoder-php/nominatim-provider/tree/5.7.0" + }, + "time": "2023-03-01T10:18:27+00:00" + }, + { + "name": "geocoder-php/plugin", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/geocoder-php/plugin.git", + "reference": "ecda7277f9c3742cd6cf4eeea01b69065a8c2d28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/geocoder-php/plugin/zipball/ecda7277f9c3742cd6cf4eeea01b69065a8c2d28", + "reference": "ecda7277f9c3742cd6cf4eeea01b69065a8c2d28", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "php-http/promise": "^1.0", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "willdurand/geocoder": "^4.0" + }, + "require-dev": { + "cache/void-adapter": "^1.0", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Geocoder\\Plugin\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "Plugins to Geocoder providers", + "homepage": "http://geocoder-php.org", + "keywords": [ + "geocoder plugin" + ], + "support": { + "source": "https://github.com/geocoder-php/plugin/tree/1.5.0" + }, + "time": "2022-07-30T13:42:14+00:00" + }, + { + "name": "giggsey/libphonenumber-for-php", + "version": "8.13.11", + "source": { + "type": "git", + "url": "https://github.com/giggsey/libphonenumber-for-php.git", + "reference": "e2395dadce92f139fc6ec71bfe8ae46a2e942944" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/giggsey/libphonenumber-for-php/zipball/e2395dadce92f139fc6ec71bfe8ae46a2e942944", + "reference": "e2395dadce92f139fc6ec71bfe8ae46a2e942944", + "shasum": "" + }, + "require": { + "giggsey/locale": "^1.7|^2.0", + "php": ">=5.3.2", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "pear/pear-core-minimal": "^1.9", + "pear/pear_exception": "^1.0", + "pear/versioncontrol_git": "^0.5", + "phing/phing": "^2.7", + "php-coveralls/php-coveralls": "^1.0|^2.0", + "symfony/console": "^2.8|^3.0|^v4.4|^v5.2", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "libphonenumber\\": "src/" + }, + "exclude-from-classmap": [ + "/src/data/", + "/src/carrier/data/", + "/src/geocoding/data/", + "/src/timezone/data/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Joshua Gigg", + "email": "giggsey@gmail.com", + "homepage": "https://giggsey.com/" + } + ], + "description": "PHP Port of Google's libphonenumber", + "homepage": "https://github.com/giggsey/libphonenumber-for-php", + "keywords": [ + "geocoding", + "geolocation", + "libphonenumber", + "mobile", + "phonenumber", + "validation" + ], + "support": { + "issues": "https://github.com/giggsey/libphonenumber-for-php/issues", + "source": "https://github.com/giggsey/libphonenumber-for-php" + }, + "time": "2023-04-27T11:06:55+00:00" + }, + { + "name": "giggsey/locale", + "version": "2.4", + "source": { + "type": "git", + "url": "https://github.com/giggsey/Locale.git", + "reference": "a6b33dfc9e8949b7e28133c4628b29cd9f1850bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/giggsey/Locale/zipball/a6b33dfc9e8949b7e28133c4628b29cd9f1850bb", + "reference": "a6b33dfc9e8949b7e28133c4628b29cd9f1850bb", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "ext-json": "*", + "pear/pear-core-minimal": "^1.9", + "pear/pear_exception": "^1.0", + "pear/versioncontrol_git": "^0.5", + "phing/phing": "^2.7", + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^8.5|^9.5", + "symfony/console": "^5.0|^6.0", + "symfony/filesystem": "^5.0|^6.0", + "symfony/finder": "^5.0|^6.0", + "symfony/process": "^5.0|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Giggsey\\Locale\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joshua Gigg", + "email": "giggsey@gmail.com", + "homepage": "https://giggsey.com/" + } + ], + "description": "Locale functions required by libphonenumber-for-php", + "support": { + "issues": "https://github.com/giggsey/Locale/issues", + "source": "https://github.com/giggsey/Locale/tree/2.4" + }, + "time": "2023-04-13T07:40:58+00:00" + }, + { + "name": "google/auth", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/googleapis/google-auth-library-php.git", + "reference": "07f7f6305f1b7df32b2acf6e101c1225c839c7ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/07f7f6305f1b7df32b2acf6e101c1225c839c7ac", + "reference": "07f7f6305f1b7df32b2acf6e101c1225c839c7ac", + "shasum": "" + }, + "require": { + "firebase/php-jwt": "^6.0", + "guzzlehttp/guzzle": "^6.2.1|^7.0", + "guzzlehttp/psr7": "^2.4.5", + "php": "^7.4||^8.0", + "psr/cache": "^1.0||^2.0||^3.0", + "psr/http-message": "^1.1||^2.0" + }, + "require-dev": { + "guzzlehttp/promises": "^1.3", + "kelvinmo/simplejwt": "0.7.0", + "phpseclib/phpseclib": "^3.0", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.0.0", + "sebastian/comparator": ">=1.2.3", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "phpseclib/phpseclib": "May be used in place of OpenSSL for signing strings or for token management. Please require version ^2." + }, + "type": "library", + "autoload": { + "psr-4": { + "Google\\Auth\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "Google Auth Library for PHP", + "homepage": "http://github.com/google/google-auth-library-php", + "keywords": [ + "Authentication", + "google", + "oauth2" + ], + "support": { + "docs": "https://googleapis.github.io/google-auth-library-php/main/", + "issues": "https://github.com/googleapis/google-auth-library-php/issues", + "source": "https://github.com/googleapis/google-auth-library-php/tree/v1.28.0" + }, + "time": "2023-05-11T21:58:18+00:00" + }, + { + "name": "google/cloud-core", + "version": "v1.51.2", + "source": { + "type": "git", + "url": "https://github.com/googleapis/google-cloud-php-core.git", + "reference": "85dc48d62143f4bbfaa34c24da95003371de7b79" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/googleapis/google-cloud-php-core/zipball/85dc48d62143f4bbfaa34c24da95003371de7b79", + "reference": "85dc48d62143f4bbfaa34c24da95003371de7b79", + "shasum": "" + }, + "require": { + "google/auth": "^1.18", + "guzzlehttp/guzzle": "^5.3|^6.5.7|^7.4.4", + "guzzlehttp/promises": "^1.3", + "guzzlehttp/psr7": "^1.7|^2.0", + "monolog/monolog": "^1.1|^2.0|^3.0", + "php": ">=7.4", + "psr/http-message": "^1.0", + "rize/uri-template": "~0.3" + }, + "require-dev": { + "erusev/parsedown": "^1.6", + "google/cloud-common-protos": "^0.4", + "google/gax": "^1.9", + "opis/closure": "^3", + "phpdocumentor/reflection": "^5.0", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.0", + "squizlabs/php_codesniffer": "2.*" + }, + "suggest": { + "opis/closure": "May be used to serialize closures to process jobs in the batch daemon. Please require version ^3.", + "symfony/lock": "Required for the Spanner cached based session pool. Please require the following commit: 3.3.x-dev#1ba6ac9" + }, + "bin": [ + "bin/google-cloud-batch" + ], + "type": "library", + "extra": { + "component": { + "id": "cloud-core", + "target": "googleapis/google-cloud-php-core.git", + "path": "Core", + "entry": "src/ServiceBuilder.php" + } + }, + "autoload": { + "psr-4": { + "Google\\Cloud\\Core\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "Google Cloud PHP shared dependency, providing functionality useful to all components.", + "support": { + "source": "https://github.com/googleapis/google-cloud-php-core/tree/v1.51.2" + }, + "time": "2023-05-05T23:01:42+00:00" + }, + { + "name": "google/cloud-storage", + "version": "v1.31.2", + "source": { + "type": "git", + "url": "https://github.com/googleapis/google-cloud-php-storage.git", + "reference": "7fe96d56856cda550b21779bb95a066b264852da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/googleapis/google-cloud-php-storage/zipball/7fe96d56856cda550b21779bb95a066b264852da", + "reference": "7fe96d56856cda550b21779bb95a066b264852da", + "shasum": "" + }, + "require": { + "google/cloud-core": "^1.51.1", + "google/crc32": "^0.2.0", + "php": ">=7.4", + "ramsey/uuid": "^4.2.3" + }, + "require-dev": { + "erusev/parsedown": "^1.6", + "google/cloud-pubsub": "^1.0", + "phpdocumentor/reflection": "^5.0", + "phpseclib/phpseclib": "^2.0||^3.0", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.0", + "squizlabs/php_codesniffer": "2.*" + }, + "suggest": { + "google/cloud-pubsub": "May be used to register a topic to receive bucket notifications.", + "phpseclib/phpseclib": "May be used in place of OpenSSL for creating signed Cloud Storage URLs. Please require version ^2." + }, + "type": "library", + "extra": { + "component": { + "id": "cloud-storage", + "target": "googleapis/google-cloud-php-storage.git", + "path": "Storage", + "entry": "src/StorageClient.php" + } + }, + "autoload": { + "psr-4": { + "Google\\Cloud\\Storage\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "Cloud Storage Client for PHP", + "support": { + "source": "https://github.com/googleapis/google-cloud-php-storage/tree/v1.31.2" + }, + "time": "2023-05-05T23:01:42+00:00" + }, + { + "name": "google/crc32", + "version": "v0.2.0", + "source": { + "type": "git", + "url": "https://github.com/google/php-crc32.git", + "reference": "948f7945d803dcc1a375152c72f63144c2dadf23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/google/php-crc32/zipball/948f7945d803dcc1a375152c72f63144c2dadf23", + "reference": "948f7945d803dcc1a375152c72f63144c2dadf23", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "v3.15", + "phpunit/phpunit": "^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "Google\\CRC32\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Andrew Brampton", + "email": "bramp@google.com" + } + ], + "description": "Various CRC32 implementations", + "homepage": "https://github.com/google/php-crc32", + "support": { + "issues": "https://github.com/google/php-crc32/issues", + "source": "https://github.com/google/php-crc32/tree/v0.2.0" + }, + "time": "2023-04-16T22:44:57+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/fb7566caccf22d74d1ab270de3551f72a58399f5", + "reference": "fb7566caccf22d74d1ab270de3551f72a58399f5", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", + "ext-curl": "*", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.29 || ^9.5.23", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2023-05-21T14:04:53+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.5.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e", + "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.5.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-05-21T12:31:43+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "b635f279edd83fc275f822a1188157ffea568ff6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/b635f279edd83fc275f822a1188157ffea568ff6", + "reference": "b635f279edd83fc275f822a1188157ffea568ff6", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.29 || ^9.5.23" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.5.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-04-17T16:11:26+00:00" + }, + { + "name": "handcraftedinthealps/goodby-csv", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/handcraftedinthealps/goodby-csv.git", + "reference": "6dc287cb3c3b8d055a0a4889661ef2aeb0bbdfa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/handcraftedinthealps/goodby-csv/zipball/6dc287cb3c3b8d055a0a4889661ef2aeb0bbdfa8", + "reference": "6dc287cb3c3b8d055a0a4889661ef2aeb0bbdfa8", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-mbstring": "^1.0" + }, + "replace": { + "goodby/csv": "self.version" + }, + "require-dev": { + "mikey179/vfsstream": "^1.1", + "mockery/mockery": "^1.0", + "phpspec/prophecy": "^1.14", + "phpunit/phpunit": "^8.5", + "rector/rector": "^0.12.5", + "suin/php-expose": "^1.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Goodby\\CSV": "src/" + }, + "exclude-from-classmap": [ + "/src/Goodby/CSV/Export/Tests/", + "/src/Goodby/CSV/Import/Tests/", + "/src/Goodby/CSV/TestHelper/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "reoring", + "email": "mori.reo@gmail.com", + "homepage": "https://www.facebook.com/reoring", + "role": "Developer" + }, + { + "name": "suin", + "email": "suinyeze@gmail.com", + "homepage": "https://www.facebook.com/suinyeze", + "role": "Developer, Renaming Specialist" + }, + { + "name": "Handcrafted in the alps contributors", + "homepage": "https://github.com/handcraftedinthealps/goodby-csv/graphs/contributors", + "role": "Contributors" + } + ], + "description": "CSV import/export library", + "homepage": "https://github.com/handcraftedinthealps/goodby-csv", + "keywords": [ + "csv", + "export", + "import" + ], + "support": { + "source": "https://github.com/handcraftedinthealps/goodby-csv/tree/1.4.2" + }, + "time": "2023-03-15T19:52:06+00:00" + }, + { + "name": "hautelook/alice-bundle", + "version": "2.12.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/AliceBundle.git", + "reference": "ebaf24ec0318583d8c022966687e2a4979bb9af8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/AliceBundle/zipball/ebaf24ec0318583d8c022966687e2a4979bb9af8", + "reference": "ebaf24ec0318583d8c022966687e2a4979bb9af8", + "shasum": "" + }, + "require": { + "doctrine/data-fixtures": "^1.5", + "doctrine/doctrine-bundle": "^2.5", + "doctrine/orm": "^2.10.0", + "doctrine/persistence": "^2.2 || ^3.0", + "php": "^8.1", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/finder": "^5.4.2 || ^6.0", + "symfony/framework-bundle": "^5.4.2 || ^6.0", + "theofidry/alice-data-fixtures": "^1.5" + }, + "require-dev": { + "phpspec/prophecy": "^1.7", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "symfony/phpunit-bridge": "^6.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + }, + "bin-dir": "bin", + "sort-packages": true + }, + "autoload": { + "psr-4": { + "Hautelook\\AliceBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Baldur Rensch", + "email": "brensch@gmail.com" + }, + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com", + "homepage": "https://github.com/theofidry" + } + ], + "description": "Symfony bundle to manage fixtures with Alice and Faker.", + "keywords": [ + "Fixture", + "alice", + "faker", + "orm", + "symfony" + ], + "support": { + "issues": "https://github.com/theofidry/AliceBundle/issues", + "source": "https://github.com/theofidry/AliceBundle/tree/2.12.0" + }, + "time": "2023-03-26T09:04:39+00:00" + }, + { + "name": "knplabs/knp-components", + "version": "v4.1.0", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/knp-components.git", + "reference": "6b6efa81ee894e325744bf785d50dc962937b1f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/knp-components/zipball/6b6efa81ee894e325744bf785d50dc962937b1f2", + "reference": "6b6efa81ee894e325744bf785d50dc962937b1f2", + "shasum": "" + }, + "require": { + "php": "^8.0", + "symfony/event-dispatcher-contracts": "^3.0" + }, + "conflict": { + "doctrine/dbal": "<2.10" + }, + "require-dev": { + "doctrine/mongodb-odm": "^2.4", + "doctrine/orm": "^2.12", + "doctrine/phpcr-odm": "^1.6", + "ext-pdo_sqlite": "*", + "jackalope/jackalope-doctrine-dbal": "^1.8", + "phpunit/phpunit": "^9.5", + "propel/propel1": "^1.7", + "ruflin/elastica": "^7.0", + "solarium/solarium": "^6.0", + "symfony/http-foundation": "^5.4 || ^6.0", + "symfony/http-kernel": "^5.4 || ^6.0", + "symfony/property-access": "^5.4 || ^6.0" + }, + "suggest": { + "doctrine/common": "to allow usage pagination with Doctrine ArrayCollection", + "doctrine/mongodb-odm": "to allow usage pagination with Doctrine ODM MongoDB", + "doctrine/orm": "to allow usage pagination with Doctrine ORM", + "doctrine/phpcr-odm": "to allow usage pagination with Doctrine ODM PHPCR", + "propel/propel1": "to allow usage pagination with Propel ORM", + "ruflin/elastica": "to allow usage pagination with ElasticSearch Client", + "solarium/solarium": "to allow usage pagination with Solarium Client", + "symfony/http-foundation": "to retrieve arguments from Request", + "symfony/property-access": "to allow sorting arrays" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Knp\\Component\\": "src/Knp/Component" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KnpLabs Team", + "homepage": "https://knplabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/KnpLabs/knp-components/contributors" + } + ], + "description": "Knplabs component library", + "homepage": "http://github.com/KnpLabs/knp-components", + "keywords": [ + "components", + "knp", + "knplabs", + "pager", + "paginator" + ], + "support": { + "issues": "https://github.com/KnpLabs/knp-components/issues", + "source": "https://github.com/KnpLabs/knp-components/tree/v4.1.0" + }, + "time": "2022-12-19T09:36:54+00:00" + }, + { + "name": "knplabs/knp-paginator-bundle", + "version": "v6.2.0", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/KnpPaginatorBundle.git", + "reference": "8da698f0856b1d9c9c02dcacbfc382c5c9440c80" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/KnpPaginatorBundle/zipball/8da698f0856b1d9c9c02dcacbfc382c5c9440c80", + "reference": "8da698f0856b1d9c9c02dcacbfc382c5c9440c80", + "shasum": "" + }, + "require": { + "knplabs/knp-components": "^4.1", + "php": "^8.0", + "symfony/config": "^6.0", + "symfony/dependency-injection": "^6.0", + "symfony/event-dispatcher": "^6.0", + "symfony/http-foundation": "^6.0", + "symfony/http-kernel": "^6.0", + "symfony/routing": "^6.0", + "symfony/translation": "^6.0", + "twig/twig": "^3.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^9.5", + "symfony/expression-language": "^6.0", + "symfony/templating": "^6.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Knp\\Bundle\\PaginatorBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KnpLabs Team", + "homepage": "https://knplabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/KnpLabs/KnpPaginatorBundle/contributors" + } + ], + "description": "Paginator bundle for Symfony to automate pagination and simplify sorting and other features", + "homepage": "https://github.com/KnpLabs/KnpPaginatorBundle", + "keywords": [ + "bundle", + "knp", + "knplabs", + "pager", + "pagination", + "paginator", + "symfony" + ], + "support": { + "issues": "https://github.com/KnpLabs/KnpPaginatorBundle/issues", + "source": "https://github.com/KnpLabs/KnpPaginatorBundle/tree/v6.2.0" + }, + "time": "2023-03-25T06:51:40+00:00" + }, + { + "name": "laminas/laminas-code", + "version": "4.11.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-code.git", + "reference": "169123b3ede20a9193480c53de2a8194f8c073ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-code/zipball/169123b3ede20a9193480c53de2a8194f8c073ec", + "reference": "169123b3ede20a9193480c53de2a8194f8c073ec", + "shasum": "" + }, + "require": { + "php": "~8.1.0 || ~8.2.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0.0", + "ext-phar": "*", + "laminas/laminas-coding-standard": "^2.3.0", + "laminas/laminas-stdlib": "^3.6.1", + "phpunit/phpunit": "^10.0.9", + "psalm/plugin-phpunit": "^0.18.4", + "vimeo/psalm": "^5.7.1" + }, + "suggest": { + "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features", + "laminas/laminas-stdlib": "Laminas\\Stdlib component" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Code\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Extensions to the PHP Reflection API, static code scanning, and code generation", + "homepage": "https://laminas.dev", + "keywords": [ + "code", + "laminas", + "laminasframework" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-code/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-code/issues", + "rss": "https://github.com/laminas/laminas-code/releases.atom", + "source": "https://github.com/laminas/laminas-code" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2023-05-14T12:05:38+00:00" + }, + { + "name": "lcobucci/jwt", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "47bdb0e0b5d00c2f89ebe33e7e384c77e84e7c34" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/47bdb0e0b5d00c2f89ebe33e7e384c77e84e7c34", + "reference": "47bdb0e0b5d00c2f89ebe33e7e384c77e84e7c34", + "shasum": "" + }, + "require": { + "ext-hash": "*", + "ext-json": "*", + "ext-openssl": "*", + "ext-sodium": "*", + "php": "~8.1.0 || ~8.2.0", + "psr/clock": "^1.0" + }, + "require-dev": { + "infection/infection": "^0.26.19", + "lcobucci/clock": "^3.0", + "lcobucci/coding-standard": "^9.0", + "phpbench/phpbench": "^1.2.8", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.10.3", + "phpstan/phpstan-deprecation-rules": "^1.1.2", + "phpstan/phpstan-phpunit": "^1.3.8", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^10.0.12" + }, + "suggest": { + "lcobucci/clock": ">= 3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "support": { + "issues": "https://github.com/lcobucci/jwt/issues", + "source": "https://github.com/lcobucci/jwt/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2023-02-25T21:35:16+00:00" + }, + { + "name": "league/commonmark", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "d44a24690f16b8c1808bf13b1bd54ae4c63ea048" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d44a24690f16b8c1808bf13b1bd54ae4c63ea048", + "reference": "d44a24690f16b8c1808bf13b1bd54ae4c63ea048", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.30.0", + "commonmark/commonmark.js": "0.30.0", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2023-03-24T15:16:10+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.15.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "a141d430414fcb8bf797a18716b09f759a385bed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a141d430414fcb8bf797a18716b09f759a385bed", + "reference": "a141d430414fcb8bf797a18716b09f759a385bed", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5", + "async-aws/simple-s3": "^1.1", + "aws/aws-sdk-php": "^3.220.0", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "microsoft/azure-storage-blob": "^1.1", + "phpseclib/phpseclib": "^3.0.14", + "phpstan/phpstan": "^0.12.26", + "phpunit/phpunit": "^9.5.11", + "sabre/dav": "^4.3.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.15.1" + }, + "funding": [ + { + "url": "https://ecologi.com/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + } + ], + "time": "2023-05-04T09:04:26+00:00" + }, + { + "name": "league/flysystem-aws-s3-v3", + "version": "3.15.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", + "reference": "d8de61ee10b6a607e7996cff388c5a3a663e8c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/d8de61ee10b6a607e7996cff388c5a3a663e8c8a", + "reference": "d8de61ee10b6a607e7996cff388c5a3a663e8c8a", + "shasum": "" + }, + "require": { + "aws/aws-sdk-php": "^3.220.0", + "league/flysystem": "^3.10.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\AwsS3V3\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "AWS S3 filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "aws", + "file", + "files", + "filesystem", + "s3", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues", + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.15.0" + }, + "funding": [ + { + "url": "https://ecologi.com/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + } + ], + "time": "2023-05-02T20:02:14+00:00" + }, + { + "name": "league/flysystem-bundle", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-bundle.git", + "reference": "4b6e8095dbb9bed9971b4a5d8158cc6d8e720a26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-bundle/zipball/4b6e8095dbb9bed9971b4a5d8158cc6d8e720a26", + "reference": "4b6e8095dbb9bed9971b4a5d8158cc6d8e720a26", + "shasum": "" + }, + "require": { + "league/flysystem": "^3.0", + "php": ">=8.0", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/options-resolver": "^5.4|^6.0" + }, + "require-dev": { + "league/flysystem-async-aws-s3": "^3.1", + "league/flysystem-aws-s3-v3": "^3.1", + "league/flysystem-azure-blob-storage": "^3.1", + "league/flysystem-ftp": "^3.1", + "league/flysystem-google-cloud-storage": "^3.1", + "league/flysystem-memory": "^3.1", + "league/flysystem-sftp-v3": "^3.1", + "symfony/dotenv": "^5.4|^6.0", + "symfony/framework-bundle": "^5.4|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "League\\FlysystemBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Titouan Galopin", + "email": "galopintitouan@gmail.com" + } + ], + "description": "Symfony bundle integrating Flysystem into Symfony 5.4+ applications", + "support": { + "issues": "https://github.com/thephpleague/flysystem-bundle/issues", + "source": "https://github.com/thephpleague/flysystem-bundle/tree/3.1.0" + }, + "time": "2022-12-26T19:09:49+00:00" + }, + { + "name": "league/flysystem-google-cloud-storage", + "version": "3.15.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-google-cloud-storage.git", + "reference": "c7949cd9919801f9820902e02d148e7158ef80e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-google-cloud-storage/zipball/c7949cd9919801f9820902e02d148e7158ef80e0", + "reference": "c7949cd9919801f9820902e02d148e7158ef80e0", + "shasum": "" + }, + "require": { + "google/cloud-storage": "^1.23", + "league/flysystem": "^3.10.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\GoogleCloudStorage\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Google Cloud Storage adapter for Flysystem.", + "keywords": [ + "Flysystem", + "filesystem", + "gcs", + "google cloud storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem-google-cloud-storage/issues", + "source": "https://github.com/thephpleague/flysystem-google-cloud-storage/tree/3.15.0" + }, + "funding": [ + { + "url": "https://ecologi.com/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + } + ], + "time": "2023-05-02T20:02:14+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.15.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "543f64c397fefdf9cfeac443ffb6beff602796b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/543f64c397fefdf9cfeac443ffb6beff602796b3", + "reference": "543f64c397fefdf9cfeac443ffb6beff602796b3", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem-local/issues", + "source": "https://github.com/thephpleague/flysystem-local/tree/3.15.0" + }, + "funding": [ + { + "url": "https://ecologi.com/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + } + ], + "time": "2023-05-02T20:02:14+00:00" + }, + { + "name": "league/flysystem-memory", + "version": "3.15.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-memory.git", + "reference": "b6caa40a84ae1d7dd9c4c1ff8b797e08790ee892" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-memory/zipball/b6caa40a84ae1d7dd9c4c1ff8b797e08790ee892", + "reference": "b6caa40a84ae1d7dd9c4c1ff8b797e08790ee892", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\InMemory\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "In-memory filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "memory" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem-memory/issues", + "source": "https://github.com/thephpleague/flysystem-memory/tree/3.15.0" + }, + "funding": [ + { + "url": "https://ecologi.com/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + } + ], + "time": "2023-05-02T20:02:14+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "ff6248ea87a9f116e78edd6002e39e5128a0d4dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ff6248ea87a9f116e78edd6002e39e5128a0d4dd", + "reference": "ff6248ea87a9f116e78edd6002e39e5128a0d4dd", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.11.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2022-04-17T13:12:02+00:00" + }, + { + "name": "league/uri", + "version": "6.8.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "a700b4656e4c54371b799ac61e300ab25a2d1d39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/a700b4656e4c54371b799ac61e300ab25a2d1d39", + "reference": "a700b4656e4c54371b799ac61e300ab25a2d1d39", + "shasum": "" + }, + "require": { + "ext-json": "*", + "league/uri-interfaces": "^2.3", + "php": "^8.1", + "psr/http-message": "^1.0.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^v3.9.5", + "nyholm/psr7": "^1.5.1", + "php-http/psr7-integration-tests": "^1.1.1", + "phpbench/phpbench": "^1.2.6", + "phpstan/phpstan": "^1.8.5", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1.1", + "phpstan/phpstan-strict-rules": "^1.4.3", + "phpunit/phpunit": "^9.5.24", + "psr/http-factory": "^1.0.1" + }, + "suggest": { + "ext-fileinfo": "Needed to create Data URI from a filepath", + "ext-intl": "Needed to improve host validation", + "league/uri-components": "Needed to easily manipulate URI objects", + "psr/http-factory": "Needed to use the URI factory" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri/issues", + "source": "https://github.com/thephpleague/uri/tree/6.8.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2022-09-13T19:58:47+00:00" + }, + { + "name": "league/uri-components", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-components.git", + "reference": "06f49e726a7fd06063e80f5f8b045fb9128e6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-components/zipball/06f49e726a7fd06063e80f5f8b045fb9128e6c43", + "reference": "06f49e726a7fd06063e80f5f8b045fb9128e6c43", + "shasum": "" + }, + "require": { + "ext-json": "*", + "league/uri-interfaces": "^2.3", + "php": "^7.4 || ^8.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^v3.8.0", + "guzzlehttp/psr7": "^2.2", + "laminas/laminas-diactoros": "^2.11.0", + "league/uri": "^6.0", + "phpstan/phpstan": "^1.7", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.2", + "phpunit/phpunit": "^9.5.20" + }, + "suggest": { + "ext-fileinfo": "Needed to create Data URI from a filepath", + "ext-gmp": "to improve handle IPV4 parsing", + "ext-intl": "to handle IDN host", + "jeremykendall/php-domain-parser": "Public Suffix and Top Level Domain parsing implemented in PHP", + "league/uri": "to allow manipulating URI objects", + "php-64bit": "to improve handle IPV4 parsing", + "psr/http-message-implementation": "to allow manipulating PSR-7 Uri objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI components manipulation library", + "homepage": "http://uri.thephpleague.com", + "keywords": [ + "authority", + "components", + "fragment", + "host", + "path", + "port", + "query", + "rfc3986", + "scheme", + "uri", + "url", + "userinfo" + ], + "support": { + "issues": "https://github.com/thephpleague/uri-components/issues", + "source": "https://github.com/thephpleague/uri-components/tree/2.4.1" + }, + "funding": [ + { + "url": "https://github.com/nyamsprod", + "type": "github" + } + ], + "time": "2022-05-26T05:50:46+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "00e7e2943f76d8cb50c7dfdc2f6dee356e15e383" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/00e7e2943f76d8cb50c7dfdc2f6dee356e15e383", + "reference": "00e7e2943f76d8cb50c7dfdc2f6dee356e15e383", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.19", + "phpstan/phpstan": "^0.12.90", + "phpstan/phpstan-phpunit": "^0.12.19", + "phpstan/phpstan-strict-rules": "^0.12.9", + "phpunit/phpunit": "^8.5.15 || ^9.5" + }, + "suggest": { + "ext-intl": "to use the IDNA feature", + "symfony/intl": "to use the IDNA feature via Symfony Polyfill" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common interface for URI representation", + "homepage": "http://github.com/thephpleague/uri-interfaces", + "keywords": [ + "rfc3986", + "rfc3987", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/thephpleague/uri-interfaces/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2021-06-28T04:27:21+00:00" + }, + { + "name": "lorenzo/pinky", + "version": "1.0.9", + "source": { + "type": "git", + "url": "https://github.com/lorenzo/pinky.git", + "reference": "f890472e4a25f89591f176aa03d9588a9d3332a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lorenzo/pinky/zipball/f890472e4a25f89591f176aa03d9588a9d3332a7", + "reference": "f890472e4a25f89591f176aa03d9588a9d3332a7", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xsl": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.21 || ^9.5.10" + }, + "type": "library", + "autoload": { + "files": [ + "src/pinky.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jose Lorenzo Rodriguez", + "email": "jose.zap@gmail.com" + } + ], + "description": "A Foundation for Emails (Inky) template transpiler", + "keywords": [ + "email", + "foundation", + "inky", + "template", + "zurb" + ], + "support": { + "issues": "https://github.com/lorenzo/pinky/issues", + "source": "https://github.com/lorenzo/pinky/tree/1.0.9" + }, + "time": "2023-01-12T16:15:52+00:00" + }, + { + "name": "masterminds/html5", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3", + "reference": "3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.8.0" + }, + "time": "2023-04-26T07:27:39+00:00" + }, + { + "name": "meilisearch/meilisearch-php", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/meilisearch/meilisearch-php.git", + "reference": "950d587995bbd72f5eb794a1f3e88eceb34f6245" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/meilisearch/meilisearch-php/zipball/950d587995bbd72f5eb794a1f3e88eceb34f6245", + "reference": "950d587995bbd72f5eb794a1f3e88eceb34f6245", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.4 || ^8.0", + "php-http/client-common": "^2.0", + "php-http/discovery": "^1.7", + "php-http/httplug": "^2.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.0", + "guzzlehttp/guzzle": "^7.1", + "http-interop/http-factory-guzzle": "^1.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "1.10.8", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^9.5" + }, + "suggest": { + "guzzlehttp/guzzle": "Use Guzzle ^7 as HTTP client", + "http-interop/http-factory-guzzle": "Factory for guzzlehttp/guzzle" + }, + "type": "library", + "autoload": { + "psr-4": { + "MeiliSearch\\": "src/", + "Meilisearch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Clementine Urquizar", + "email": "clementine@meilisearch.com" + } + ], + "description": "PHP wrapper for the Meilisearch API", + "keywords": [ + "api", + "client", + "instant", + "meilisearch", + "php", + "search" + ], + "support": { + "issues": "https://github.com/meilisearch/meilisearch-php/issues", + "source": "https://github.com/meilisearch/meilisearch-php/tree/v1.1.1" + }, + "time": "2023-04-03T10:05:59+00:00" + }, + { + "name": "mollie/mollie-api-php", + "version": "v2.55.0", + "source": { + "type": "git", + "url": "https://github.com/mollie/mollie-api-php.git", + "reference": "a2be26bd83dd33ea187a2238fbe0b30582c50774" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mollie/mollie-api-php/zipball/a2be26bd83dd33ea187a2238fbe0b30582c50774", + "reference": "a2be26bd83dd33ea187a2238fbe0b30582c50774", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.2", + "ext-curl": "*", + "ext-json": "*", + "ext-openssl": "*", + "php": "^7.2|^8.0" + }, + "require-dev": { + "eloquent/liberator": "^2.0||^3.0", + "friendsofphp/php-cs-fixer": "^3.0", + "guzzlehttp/guzzle": "^6.3 || ^7.0", + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^8.5 || ^9.5" + }, + "suggest": { + "mollie/oauth2-mollie-php": "Use OAuth to authenticate with the Mollie API. This is needed for some endpoints. Visit https://docs.mollie.com/ for more information." + }, + "type": "library", + "autoload": { + "psr-4": { + "Mollie\\Api\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Mollie B.V.", + "email": "info@mollie.com" + } + ], + "description": "Mollie API client library for PHP. Mollie is a European Payment Service provider and offers international payment methods such as Mastercard, VISA, American Express and PayPal, and local payment methods such as iDEAL, Bancontact, SOFORT Banking, SEPA direct debit, Belfius Direct Net, KBC Payment Button and various gift cards such as Podiumcadeaukaart and fashioncheque.", + "homepage": "https://www.mollie.com/en/developers", + "keywords": [ + "Apple Pay", + "CBC", + "Przelewy24", + "api", + "bancontact", + "banktransfer", + "belfius", + "belfius direct net", + "charges", + "creditcard", + "direct debit", + "fashioncheque", + "gateway", + "gift cards", + "ideal", + "inghomepay", + "intersolve", + "kbc", + "klarna", + "mistercash", + "mollie", + "paylater", + "payment", + "payments", + "paypal", + "paysafecard", + "podiumcadeaukaart", + "recurring", + "refunds", + "sepa", + "service", + "sliceit", + "sofort", + "sofortbanking", + "subscriptions" + ], + "support": { + "issues": "https://github.com/mollie/mollie-api-php/issues", + "source": "https://github.com/mollie/mollie-api-php/tree/v2.55.0" + }, + "time": "2023-05-09T10:09:56+00:00" + }, + { + "name": "moneyphp/money", + "version": "v3.3.3", + "source": { + "type": "git", + "url": "https://github.com/moneyphp/money.git", + "reference": "0dc40e3791c67e8793e3aa13fead8cf4661ec9cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/moneyphp/money/zipball/0dc40e3791c67e8793e3aa13fead8cf4661ec9cd", + "reference": "0dc40e3791c67e8793e3aa13fead8cf4661ec9cd", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.6" + }, + "require-dev": { + "cache/taggable-cache": "^0.4.0", + "doctrine/instantiator": "^1.0.5", + "ext-bcmath": "*", + "ext-gmp": "*", + "ext-intl": "*", + "florianv/exchanger": "^1.0", + "florianv/swap": "^3.0", + "friends-of-phpspec/phpspec-code-coverage": "^3.1.1 || ^4.3", + "moneyphp/iso-currencies": "^3.2.1", + "php-http/message": "^1.4", + "php-http/mock-client": "^1.0.0", + "phpspec/phpspec": "^3.4.3", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.18 || ^8.5", + "psr/cache": "^1.0", + "symfony/phpunit-bridge": "^4" + }, + "suggest": { + "ext-bcmath": "Calculate without integer limits", + "ext-gmp": "Calculate without integer limits", + "ext-intl": "Format Money objects with intl", + "florianv/exchanger": "Exchange rates library for PHP", + "florianv/swap": "Exchange rates library for PHP", + "psr/cache-implementation": "Used for Currency caching" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Money\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Verraes", + "email": "mathias@verraes.net", + "homepage": "http://verraes.net" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + }, + { + "name": "Frederik Bosch", + "email": "f.bosch@genkgo.nl" + } + ], + "description": "PHP implementation of Fowler's Money pattern", + "homepage": "http://moneyphp.org", + "keywords": [ + "Value Object", + "money", + "vo" + ], + "support": { + "issues": "https://github.com/moneyphp/money/issues", + "source": "https://github.com/moneyphp/money/tree/v3.3.3" + }, + "time": "2022-09-21T07:43:36+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.3.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "9b5daeaffce5b926cac47923798bba91059e60e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/9b5daeaffce5b926cac47923798bba91059e60e2", + "reference": "9b5daeaffce5b926cac47923798bba91059e60e2", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2@dev", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-strict-rules": "^1.4", + "phpunit/phpunit": "^9.5.26", + "predis/predis": "^1.1 || ^2", + "ruflin/elastica": "^7", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.3.1" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2023-02-06T13:46:10+00:00" + }, + { + "name": "mtdowling/jmespath.php", + "version": "2.6.1", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/9b87907a81b87bc76d19a7fb2d61e61486ee9edb", + "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^1.4 || ^2.0", + "phpunit/phpunit": "^4.8.36 || ^7.5.15" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "files": [ + "src/JmesPath.php" + ], + "psr-4": { + "JmesPath\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "support": { + "issues": "https://github.com/jmespath/jmespath.php/issues", + "source": "https://github.com/jmespath/jmespath.php/tree/2.6.1" + }, + "time": "2021-06-14T00:11:39+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "nelmio/alice", + "version": "3.12.2", + "source": { + "type": "git", + "url": "https://github.com/nelmio/alice.git", + "reference": "a020c0767e10dbb7bf1c193e16e94710691133d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nelmio/alice/zipball/a020c0767e10dbb7bf1c193e16e94710691133d9", + "reference": "a020c0767e10dbb7bf1c193e16e94710691133d9", + "shasum": "" + }, + "require": { + "fakerphp/faker": "^1.10", + "myclabs/deep-copy": "^1.10", + "php": "^8.1", + "sebastian/comparator": "^3.0 || ^4.0 || ^5.0", + "symfony/property-access": "^5.4 || ^6.0", + "symfony/yaml": "^5.4 || ^6.0" + }, + "conflict": { + "symfony/framework-bundle": "<5.4.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "phpspec/prophecy": "^1.6", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.3", + "symfony/config": "^5.4 || ^6.0", + "symfony/dependency-injection": "^5.4 || ^6.0", + "symfony/finder": "^5.4 || ^6.0", + "symfony/http-kernel": "^5.4 || ^6.0", + "symfony/phpunit-bridge": "^5.4 || ^6.0", + "symfony/var-dumper": "^5.4 || ^6.0" + }, + "suggest": { + "theofidry/alice-data-fixtures": "Wrapper for Alice to provide a persistence layer." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false + }, + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "files": [ + "src/deep_clone.php" + ], + "psr-4": { + "Nelmio\\Alice\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + }, + { + "name": "Tim Shelburne", + "email": "shelburt02@gmail.com" + }, + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Expressive fixtures generator", + "keywords": [ + "Fixture", + "data", + "faker", + "test" + ], + "support": { + "issues": "https://github.com/nelmio/alice/issues", + "source": "https://github.com/nelmio/alice/tree/3.12.2" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2023-02-13T11:17:55+00:00" + }, + { + "name": "nelmio/cors-bundle", + "version": "2.3.1", + "source": { + "type": "git", + "url": "https://github.com/nelmio/NelmioCorsBundle.git", + "reference": "185d2c0ae50a3f0b628790170164d5f1c5b7c281" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/185d2c0ae50a3f0b628790170164d5f1c5b7c281", + "reference": "185d2c0ae50a3f0b628790170164d5f1c5b7c281", + "shasum": "" + }, + "require": { + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/framework-bundle": "^4.4 || ^5.4 || ^6.0" + }, + "require-dev": { + "mockery/mockery": "^1.2", + "symfony/phpunit-bridge": "^4.4 || ^5.4 || ^6.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Nelmio\\CorsBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nelmio", + "homepage": "http://nelm.io" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/nelmio/NelmioCorsBundle/contributors" + } + ], + "description": "Adds CORS (Cross-Origin Resource Sharing) headers support in your Symfony application", + "keywords": [ + "api", + "cors", + "crossdomain" + ], + "support": { + "issues": "https://github.com/nelmio/NelmioCorsBundle/issues", + "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.3.1" + }, + "time": "2023-02-16T08:49:29+00:00" + }, + { + "name": "nesbot/carbon", + "version": "2.66.0", + "source": { + "type": "git", + "url": "https://github.com/briannesbitt/Carbon.git", + "reference": "496712849902241f04902033b0441b269effe001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/496712849902241f04902033b0441b269effe001", + "reference": "496712849902241f04902033b0441b269effe001", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.1.8 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.16", + "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" + }, + "require-dev": { + "doctrine/dbal": "^2.0 || ^3.1.4", + "doctrine/orm": "^2.7", + "friendsofphp/php-cs-fixer": "^3.0", + "kylekatarnls/multi-tester": "^2.0", + "ondrejmirtes/better-reflection": "*", + "phpmd/phpmd": "^2.9", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12.99 || ^1.7.14", + "phpunit/php-file-iterator": "^2.0.5 || ^3.0.6", + "phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20", + "squizlabs/php_codesniffer": "^3.4" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.x-dev", + "dev-master": "2.x-dev" + }, + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/briannesbitt/Carbon/issues", + "source": "https://github.com/briannesbitt/Carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2023-01-29T18:53:47+00:00" + }, + { + "name": "nette/schema", + "version": "v1.2.3", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/abbdbb70e0245d5f3bf77874cea1dfb0c930d06f", + "reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f", + "shasum": "" + }, + "require": { + "nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", + "php": ">=7.1 <8.3" + }, + "require-dev": { + "nette/tester": "^2.3 || ^2.4", + "phpstan/phpstan-nette": "^1.0", + "tracy/tracy": "^2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.2.3" + }, + "time": "2022-10-13T01:24:26+00:00" + }, + { + "name": "nette/utils", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "cacdbf5a91a657ede665c541eda28941d4b09c1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/cacdbf5a91a657ede665c541eda28941d4b09c1e", + "reference": "cacdbf5a91a657ede665c541eda28941d4b09c1e", + "shasum": "" + }, + "require": { + "php": ">=8.0 <8.3" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()", + "ext-xml": "to use Strings::length() etc. when mbstring is not available" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.0.0" + }, + "time": "2023-02-02T10:41:53+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "3cb4d163b58589e47b35103e8e5e6a6a475b47be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/3cb4d163b58589e47b35103e8e5e6a6a475b47be", + "reference": "3cb4d163b58589e47b35103e8e5e6a6a475b47be", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.0" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2023-05-02T11:26:24+00:00" + }, + { + "name": "odolbeau/phone-number-bundle", + "version": "v3.9.1", + "source": { + "type": "git", + "url": "https://github.com/odolbeau/phone-number-bundle.git", + "reference": "a4c084d9709a416511234b49d6803d76b64dbc02" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/odolbeau/phone-number-bundle/zipball/a4c084d9709a416511234b49d6803d76b64dbc02", + "reference": "a4c084d9709a416511234b49d6803d76b64dbc02", + "shasum": "" + }, + "require": { + "giggsey/libphonenumber-for-php": "^8.0", + "php": ">=7.4", + "symfony/framework-bundle": "^4.4|^5.3|^6.0", + "symfony/intl": "^4.4|^5.3|^6.0" + }, + "conflict": { + "symfony/serializer": "6.0.0" + }, + "replace": { + "misd/phone-number-bundle": "self.version" + }, + "require-dev": { + "doctrine/doctrine-bundle": "^1.12|^2.0", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "symfony/form": "^4.4|^5.3|^6.0", + "symfony/property-access": "^4.4|^5.3|^6.0", + "symfony/serializer": "^4.4|^5.3|^6.0.1", + "symfony/twig-bundle": "^4.4|^5.3|^6.0", + "symfony/validator": "^4.4|^5.3|^6.0" + }, + "suggest": { + "doctrine/doctrine-bundle": "Add a DBAL mapping type", + "symfony/form": "Add a data transformer", + "symfony/property-access": "Choose a path in the validation constraint", + "symfony/serializer": "Serialize/deserialize phone numbers using Symfony library", + "symfony/twig-bundle": "Format phone numbers in Twig templates", + "symfony/validator": "Add a validation constraint" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Misd\\PhoneNumberBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Integrates libphonenumber into your Symfony application", + "homepage": "https://github.com/odolbeau/phone-number-bundle", + "keywords": [ + "bundle", + "libphonenumber", + "phone-number", + "phonenumber", + "telephone number" + ], + "support": { + "issues": "https://github.com/odolbeau/phone-number-bundle/issues", + "source": "https://github.com/odolbeau/phone-number-bundle/tree/v3.9.1" + }, + "time": "2023-01-16T11:06:41+00:00" + }, + { + "name": "payum/core", + "version": "1.7.3", + "target-dir": "Payum/Core", + "source": { + "type": "git", + "url": "https://github.com/Payum/Core.git", + "reference": "37b75090d77245569c481c4b9240a9cf01e0e8c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Payum/Core/zipball/37b75090d77245569c481c4b9240a9cf01e0e8c8", + "reference": "37b75090d77245569c481c4b9240a9cf01e0e8c8", + "shasum": "" + }, + "require": { + "alcohol/iso4217": "^3.1 || ^4.0", + "league/uri": "^6.4", + "league/uri-components": "^2.2", + "payum/iso4217": "^1.0", + "php": "^7.2 || ^8.0", + "php-http/client-implementation": "^1.0", + "php-http/message": "^1.0", + "psr/log": "^1 || ^2 || ^3", + "twig/twig": "^1.34|^2.4|^3.0" + }, + "require-dev": { + "defuse/php-encryption": "^2", + "doctrine/dbal": "^2", + "doctrine/orm": "2.*", + "doctrine/persistence": "^1.3.3|^2.0", + "ext-curl": "*", + "ext-pdo_sqlite": "*", + "laminas/laminas-db": "^2.0", + "omnipay/common": "^3.0", + "omnipay/dummy": "^3.0", + "payum/omnipay-v3-bridge": "^1.0", + "php-http/guzzle6-adapter": "^1.0", + "phpunit/phpunit": "^5.7", + "propel/propel1": "~1.7", + "symfony/cache": "^4.4|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/form": "^4.4|^5.0", + "symfony/http-foundation": "^4.4|^5.0", + "symfony/http-kernel": "^4.4|^5.0", + "symfony/phpunit-bridge": "^4.4|^5.0", + "symfony/routing": "^4.4|^5.0", + "symfony/validator": "^4.4|^5.0" + }, + "suggest": { + "defuse/php-encryption": "^2 If you want to encrypt gateways credentials in database", + "doctrine/mongodb-odm": "~2.0 If you want to store models to mongo doctrin2 ODM", + "doctrine/orm": "~2.3 If you want to store models to database using doctrin2 ORM", + "laminas/laminas-db": "~2.0 If you want to store models to Laminas Db ORM", + "monolog/monolog": "~1.0 If you want to use PSR-3 logger", + "payum/authorize-net-aim": "self.version If you want to use Authorize.Net AIM payment gateway", + "payum/be2bill": "self.version If you want to use be2bill payment gateway", + "payum/omnipay-v3-bridge": "^1 If you want to use omnipay's gateways", + "payum/payex": "self.version If you want to use payex payment gateway", + "payum/paypal-express-checkout-nvp": "self.version If you want to use paypal express checkout, digital goods or recurring payments", + "payum/paypal-ipn": "self.version If you want to use paypal instant payment notifications(Paypal IPN)", + "payum/paypal-pro-checkout-nvp": "self.version If you want to use paypal pro checkout", + "payum/paypal-rest": "self.version If you want to use paypal rest gateway", + "propel/propel": "If you want to store models to Propel2 ORM", + "propel/propel1": "~1.7 If you want to store models to Propel1 ORM", + "symfony/dependency-injection": "~2.8|~3.0 If you want to use container aware stuff", + "symfony/form": "~2.8|~3.0 If you want to use forms", + "symfony/http-foundation": "~2.8|~3.0 If you want to use HttpRequestVerifier or HttpResponse reply from symfony's bridge", + "symfony/http-kernel": "~2.8|~3.0 If you want to use HttpRequestVerifier from symfony's bridge", + "symfony/routing": "~2.8|~3.0 If you want to use TokenFactory from symfony's bridge" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6-dev" + } + }, + "autoload": { + "psr-0": { + "Payum\\Core\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kotlyar Maksim", + "email": "kotlyar.maksim@gmail.com" + }, + { + "name": "Payum project", + "homepage": "https://payum.forma-pro.com/" + }, + { + "name": "Community contributions", + "homepage": "https://github.com/Payum/Payum/contributors" + } + ], + "description": "One million downloads of Payum already! Payum offers everything you need to work with payments. Friendly for all PHP frameworks (Symfony, Laravel, Laminas, Yii, Silex). Check more visiting site.", + "homepage": "https://payum.forma-pro.com/", + "keywords": [ + "authorize", + "capture", + "notify", + "payment", + "payout", + "recurring payment", + "refund", + "subscription", + "withdrawal" + ], + "support": { + "source": "https://github.com/Payum/Core/tree/1.7.3" + }, + "time": "2022-07-04T09:47:14+00:00" + }, + { + "name": "payum/iso4217", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/Payum/iso4217.git", + "reference": "faafdd2c5e799c673d7aa576aaf26fa2fb631014" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Payum/iso4217/zipball/faafdd2c5e799c673d7aa576aaf26fa2fb631014", + "reference": "faafdd2c5e799c673d7aa576aaf26fa2fb631014", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Payum\\ISO4217\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com" + }, + { + "name": "Kotlyar Maksim", + "email": "kotlyar.maksim@gmail.com" + }, + { + "name": "Payum project", + "homepage": "http://payum.org/" + }, + { + "name": "Community contributions", + "homepage": "https://github.com/Payum/Payum/contributors" + } + ], + "description": "ISO 4217 PHP Library", + "homepage": "https://payum.forma-pro.com/", + "keywords": [ + "4217", + "ISO 4217", + "currencies", + "iso", + "library" + ], + "support": { + "issues": "https://github.com/payum/iso4217/issues", + "source": "https://github.com/payum/iso4217" + }, + "time": "2022-02-27T10:14:23+00:00" + }, + { + "name": "payum/offline", + "version": "1.7.3", + "target-dir": "Payum/Offline", + "source": { + "type": "git", + "url": "https://github.com/Payum/Offline.git", + "reference": "0600b0eb3cff8f19a351a0b3c7d29743c9cdbb20" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Payum/Offline/zipball/0600b0eb3cff8f19a351a0b3c7d29743c9cdbb20", + "reference": "0600b0eb3cff8f19a351a0b3c7d29743c9cdbb20", + "shasum": "" + }, + "require": { + "payum/core": "^1.5" + }, + "require-dev": { + "payum/core": "^1.5", + "phpunit/phpunit": "^5.7", + "symfony/phpunit-bridge": "^3.1|^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6-dev" + } + }, + "autoload": { + "psr-0": { + "Payum\\Offline": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kotlyar Maksim", + "email": "kotlyar.maksim@gmail.com" + }, + { + "name": "Payum project", + "homepage": "https://payum.forma-pro.com/" + }, + { + "name": "Community contributions", + "homepage": "https://github.com/Payum/Offline/contributors" + } + ], + "description": "The Payum extension. It provides Offline payment integration.", + "homepage": "https://payum.forma-pro.com", + "keywords": [ + "invoice", + "offlile", + "payment" + ], + "support": { + "source": "https://github.com/Payum/Offline/tree/1.7.3" + }, + "time": "2021-05-17T19:26:36+00:00" + }, + { + "name": "payum/payum-bundle", + "version": "2.5.1", + "source": { + "type": "git", + "url": "https://github.com/Payum/PayumBundle.git", + "reference": "9e1765add77c32c879d937444c89c3e044d35f21" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Payum/PayumBundle/zipball/9e1765add77c32c879d937444c89c3e044d35f21", + "reference": "9e1765add77c32c879d937444c89c3e044d35f21", + "shasum": "" + }, + "require": { + "payum/core": "^1.7.2", + "php": "^7.4 || ^8.0", + "symfony/form": "^4.4.20 || ^5.4 || ^6.0", + "symfony/framework-bundle": "^4.4 || ^5.4 || ^6.0", + "symfony/polyfill-php80": "^1.26", + "symfony/security-csrf": "^4.4 || ^5.4 || ^6.0", + "symfony/validator": "^4.4 || ^5.4 || ^6.0" + }, + "require-dev": { + "defuse/php-encryption": "^2", + "doctrine/orm": "^2.8", + "omnipay/common": "^3@dev", + "omnipay/dummy": "^3@alpha", + "omnipay/paypal": "^3@dev", + "payum/offline": "^1.7", + "payum/omnipay-v3-bridge": "^1@alpha", + "payum/paypal-express-checkout-nvp": "^1.7", + "payum/stripe": "^1.7", + "php-http/guzzle7-adapter": "^1.0", + "phpunit/phpunit": "^9.5", + "psr/log": "^1 || ^2", + "stripe/stripe-php": "~7.0", + "symfony/browser-kit": "^4.4 || ^5.4 || ^6.0", + "symfony/expression-language": "^4.4 || ^5.4 || ^6.0", + "symfony/phpunit-bridge": "^4.4 || ^5.4 || ^6.0", + "symfony/templating": "^4.4 || ^5.4 || ^6.0", + "symfony/twig-bundle": "^4.4 || ^5.4 || ^6.0", + "symfony/web-profiler-bundle": "^4.4 || ^5.4 || ^6.0", + "symfony/yaml": "^4.4 || ^5.4 || ^6.0", + "twig/twig": "^2.0 || ^3.0" + }, + "suggest": { + "sonata-project/admin-bundle": "^3 If you want to configure payments in the backend." + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev" + } + }, + "autoload": { + "psr-4": { + "Payum\\Bundle\\PayumBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kotlyar Maksim", + "email": "kotlyar.maksim@gmail.com" + }, + { + "name": "Payum project", + "homepage": "https://payum.forma-pro.com/" + }, + { + "name": "Community contributions", + "homepage": "https://github.com/Payum/PayumBundle/contributors" + } + ], + "description": "One million downloads of Payum already! Payum offers everything you need to work with payments. Check more visiting site.", + "homepage": "https://payum.forma-pro.com/", + "keywords": [ + "authorize.net", + "be2bill", + "instant notifications", + "klarna", + "offline", + "omnipay", + "payex", + "payment", + "paypal", + "paypal express checkout", + "paypal pro checkout", + "recurring payment", + "stripe", + "stripe checkout", + "stripe.js", + "symfony" + ], + "support": { + "issues": "https://github.com/Payum/PayumBundle/issues", + "source": "https://github.com/Payum/PayumBundle/tree/2.5.1" + }, + "time": "2023-05-03T10:39:27+00:00" + }, + { + "name": "php-http/client-common", + "version": "2.6.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/client-common.git", + "reference": "665bfc381bb910385f70391ed3eeefd0b7bbdd0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/client-common/zipball/665bfc381bb910385f70391ed3eeefd0b7bbdd0d", + "reference": "665bfc381bb910385f70391ed3eeefd0b7bbdd0d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/httplug": "^2.0", + "php-http/message": "^1.6", + "php-http/message-factory": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "doctrine/instantiator": "^1.1", + "guzzlehttp/psr7": "^1.4", + "nyholm/psr7": "^1.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "phpspec/prophecy": "^1.10.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" + }, + "suggest": { + "ext-json": "To detect JSON responses with the ContentTypePlugin", + "ext-libxml": "To detect XML responses with the ContentTypePlugin", + "php-http/cache-plugin": "PSR-6 Cache plugin", + "php-http/logger-plugin": "PSR-3 Logger plugin", + "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\Common\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Common HTTP Client implementations and tools for HTTPlug", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "common", + "http", + "httplug" + ], + "support": { + "issues": "https://github.com/php-http/client-common/issues", + "source": "https://github.com/php-http/client-common/tree/2.6.1" + }, + "time": "2023-04-14T13:30:08+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.18.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "29ae6fae35f4116bbfe4c8b96ccc3f687eb07cd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/29ae6fae35f4116bbfe4c8b96ccc3f687eb07cd9", + "reference": "29ae6fae35f4116bbfe4c8b96ccc3f687eb07cd9", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "symfony/phpunit-bridge": "^6.2" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.18.0" + }, + "time": "2023-05-03T14:49:12+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "625ad742c360c8ac580fcc647a1541d29e257f67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/625ad742c360c8ac580fcc647a1541d29e257f67", + "reference": "625ad742c360c8ac580fcc647a1541d29e257f67", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/2.4.0" + }, + "time": "2023-04-14T15:10:03+00:00" + }, + { + "name": "php-http/message", + "version": "1.15.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/message.git", + "reference": "2a1fbaa00cf5ffc82f379adf47388663bce8190d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message/zipball/2a1fbaa00cf5ffc82f379adf47388663bce8190d", + "reference": "2a1fbaa00cf5ffc82f379adf47388663bce8190d", + "shasum": "" + }, + "require": { + "clue/stream-filter": "^1.5", + "php": "^7.2 || ^8.0", + "php-http/message-factory": "^1.0.2", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.6", + "ext-zlib": "*", + "guzzlehttp/psr7": "^1.0 || ^2.0", + "laminas/laminas-diactoros": "^2.0 || ^3.0", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "slim/slim": "^3.0" + }, + "suggest": { + "ext-zlib": "Used with compressor/decompressor streams", + "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", + "laminas/laminas-diactoros": "Used with Diactoros Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation" + }, + "type": "library", + "autoload": { + "files": [ + "src/filters.php" + ], + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "HTTP Message related tools", + "homepage": "http://php-http.org", + "keywords": [ + "http", + "message", + "psr-7" + ], + "support": { + "issues": "https://github.com/php-http/message/issues", + "source": "https://github.com/php-http/message/tree/1.15.0" + }, + "time": "2023-05-10T08:19:58+00:00" + }, + { + "name": "php-http/message-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/message-factory.git", + "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message-factory/zipball/4d8778e1c7d405cbb471574821c1ff5b68cc8f57", + "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Factory interfaces for PSR-7 HTTP Message", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "stream", + "uri" + ], + "support": { + "issues": "https://github.com/php-http/message-factory/issues", + "source": "https://github.com/php-http/message-factory/tree/1.1.0" + }, + "abandoned": "psr/http-factory", + "time": "2023-04-14T14:16:17+00:00" + }, + { + "name": "php-http/promise", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", + "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2", + "phpspec/phpspec": "^5.1.2 || ^6.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.1.0" + }, + "time": "2020-07-07T09:29:14+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2", + "psalm/phar": "^4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + }, + "time": "2021-10-19T17:43:47+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.7.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "dfc078e8af9c99210337325ff5aa152872c98714" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/dfc078e8af9c99210337325ff5aa152872c98714", + "reference": "dfc078e8af9c99210337325ff5aa152872c98714", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.13" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.1" + }, + "time": "2023-03-27T19:02:04+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.20.4", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd", + "reference": "7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.20.4" + }, + "time": "2023-05-02T09:19:37+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31", + "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/1.0.2" + }, + "time": "2023-04-10T20:12:12+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "e616d01114759c4c489f93b099585439f795fe35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + }, + "time": "2023-04-10T20:10:41+00:00" + }, + { + "name": "psr/http-message", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "time": "2023-04-04T09:50:52+00:00" + }, + { + "name": "psr/link", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/link.git", + "reference": "84b159194ecfd7eaa472280213976e96415433f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/link/zipball/84b159194ecfd7eaa472280213976e96415433f7", + "reference": "84b159194ecfd7eaa472280213976e96415433f7", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "suggest": { + "fig/link-util": "Provides some useful PSR-13 utilities" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Link\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for HTTP links", + "homepage": "https://github.com/php-fig/link", + "keywords": [ + "http", + "http-link", + "link", + "psr", + "psr-13", + "rest" + ], + "support": { + "source": "https://github.com/php-fig/link/tree/2.0.1" + }, + "time": "2021-03-11T23:00:27+00:00" + }, + { + "name": "psr/log", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.0" + }, + "time": "2021-07-14T16:46:02+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.28.3", + "fakerphp/faker": "^1.21", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^1.0", + "mockery/mockery": "^1.5", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpcsstandards/phpcsutils": "^1.0.0-rc1", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.18.4", + "ramsey/coding-standard": "^2.0.3", + "ramsey/conventional-commits": "^1.3", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", + "type": "tidelift" + } + ], + "time": "2022-12-31T21:50:55+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.7.4", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "60a4c63ab724854332900504274f6150ff26d286" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/60a4c63ab724854332900504274f6150ff26d286", + "reference": "60a4c63ab724854332900504274f6150ff26d286", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11", + "ext-json": "*", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.10", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.8", + "ergebnis/composer-normalize": "^2.15", + "mockery/mockery": "^1.3", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.2", + "php-mock/php-mock-mockery": "^1.3", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^8.5 || ^9", + "ramsey/composer-repl": "^1.4", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.9" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.7.4" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" + } + ], + "time": "2023-04-15T23:01:58+00:00" + }, + { + "name": "rize/uri-template", + "version": "0.3.5", + "source": { + "type": "git", + "url": "https://github.com/rize/UriTemplate.git", + "reference": "5ed4ba8ea34af84485dea815d4b6b620794d1168" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rize/UriTemplate/zipball/5ed4ba8ea34af84485dea815d4b6b620794d1168", + "reference": "5ed4ba8ea34af84485dea815d4b6b620794d1168", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "Rize\\": "src/Rize" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marut K", + "homepage": "http://twitter.com/rezigned" + } + ], + "description": "PHP URI Template (RFC 6570) supports both expansion & extraction", + "keywords": [ + "RFC 6570", + "template", + "uri" + ], + "support": { + "issues": "https://github.com/rize/UriTemplate/issues", + "source": "https://github.com/rize/UriTemplate/tree/0.3.5" + }, + "funding": [ + { + "url": "https://www.paypal.me/rezigned", + "type": "custom" + }, + { + "url": "https://github.com/rezigned", + "type": "github" + }, + { + "url": "https://opencollective.com/rize-uri-template", + "type": "open_collective" + } + ], + "time": "2022-10-12T17:22:51+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-05-07T05:35:17+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T06:03:37+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sensio/framework-extra-bundle", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/sensiolabs/SensioFrameworkExtraBundle.git", + "reference": "2f886f4b31f23c76496901acaedfedb6936ba61f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sensiolabs/SensioFrameworkExtraBundle/zipball/2f886f4b31f23c76496901acaedfedb6936ba61f", + "reference": "2f886f4b31f23c76496901acaedfedb6936ba61f", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0|^2.0", + "php": ">=7.2.5", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/framework-bundle": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0" + }, + "conflict": { + "doctrine/doctrine-cache-bundle": "<1.3.1", + "doctrine/persistence": "<1.3" + }, + "require-dev": { + "doctrine/dbal": "^2.10|^3.0", + "doctrine/doctrine-bundle": "^1.11|^2.0", + "doctrine/orm": "^2.5", + "symfony/browser-kit": "^4.4|^5.0|^6.0", + "symfony/doctrine-bridge": "^4.4|^5.0|^6.0", + "symfony/dom-crawler": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", + "symfony/monolog-bridge": "^4.0|^5.0|^6.0", + "symfony/monolog-bundle": "^3.2", + "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0", + "symfony/security-bundle": "^4.4|^5.0|^6.0", + "symfony/twig-bundle": "^4.4|^5.0|^6.0", + "symfony/yaml": "^4.4|^5.0|^6.0", + "twig/twig": "^1.34|^2.4|^3.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "6.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sensio\\Bundle\\FrameworkExtraBundle\\": "src/" + }, + "exclude-from-classmap": [ + "/tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "This bundle provides a way to configure your controllers with annotations", + "keywords": [ + "annotations", + "controllers" + ], + "support": { + "source": "https://github.com/sensiolabs/SensioFrameworkExtraBundle/tree/v6.2.10" + }, + "abandoned": "Symfony", + "time": "2023-02-24T14:57:12+00:00" + }, + { + "name": "snc/redis-bundle", + "version": "4.6.0", + "source": { + "type": "git", + "url": "https://github.com/snc/SncRedisBundle.git", + "reference": "dc5f4438d669f1c52234f1d2b7bc558f8399c58e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/snc/SncRedisBundle/zipball/dc5f4438d669f1c52234f1d2b7bc558f8399c58e", + "reference": "dc5f4438d669f1c52234f1d2b7bc558f8399c58e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "symfony/deprecation-contracts": "^2 || ^3", + "symfony/framework-bundle": "^4.4 || ^5.3 || ^6.0", + "symfony/http-foundation": "^4.4 || ^5.3 || ^6.0", + "symfony/service-contracts": ">=1.0", + "symfony/var-dumper": "^4.4 || ^5.3 || ^6.0" + }, + "conflict": { + "ext-redis": "<5.3", + "predis/predis": "<2.0 || >=3.0" + }, + "require-dev": { + "doctrine/annotations": "^1.13", + "doctrine/coding-standard": "^10.0", + "ext-pdo_sqlite": "*", + "ext-redis": "*", + "friendsofphp/proxy-manager-lts": "^1.0.6", + "monolog/monolog": "*", + "phpunit/phpunit": "^8.5.32 || ^9.5.28", + "predis/predis": "^2.0", + "symfony/browser-kit": "^4.4 || ^5.3 || ^6.0", + "symfony/cache": "^4.4 || ^5.3 || ^6.0", + "symfony/console": "^4.4 || ^5.3 || ^6.0", + "symfony/dom-crawler": "^4.4 || ^5.3 || ^6.0", + "symfony/filesystem": "^4.4 || ^5.3 || ^6.0", + "symfony/phpunit-bridge": "^6.0", + "symfony/profiler-pack": "^1.0", + "symfony/proxy-manager-bridge": "^4.4 || ^5.3 || ^6.0", + "symfony/stopwatch": "^4.4 || ^5.3 || ^6.0", + "symfony/twig-bundle": "^4.4 || ^5.3 || ^6.0", + "symfony/yaml": "^4.4 || ^5.3 || ^6.0", + "vimeo/psalm": "^5.2" + }, + "suggest": { + "monolog/monolog": "If you want to use the monolog redis handler.", + "predis/predis": "If you want to use predis.", + "symfony/console": "If you want to use commands to interact with the redis database", + "symfony/proxy-manager-bridge": "If you want to lazy-load some services" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Snc\\RedisBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Henrik Westphal", + "email": "henrik.westphal@gmail.com" + }, + { + "name": "Community contributors", + "homepage": "https://github.com/snc/SncRedisBundle/contributors" + } + ], + "description": "A Redis bundle for Symfony", + "homepage": "https://github.com/snc/SncRedisBundle", + "keywords": [ + "nosql", + "redis", + "symfony" + ], + "support": { + "issues": "https://github.com/snc/SncRedisBundle/issues", + "source": "https://github.com/snc/SncRedisBundle/tree/4.6.0" + }, + "time": "2023-01-29T15:18:19+00:00" + }, + { + "name": "stof/doctrine-extensions-bundle", + "version": "v1.7.1", + "source": { + "type": "git", + "url": "https://github.com/stof/StofDoctrineExtensionsBundle.git", + "reference": "fa650e60e174afa06c09e28a54fb1854af04c7fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stof/StofDoctrineExtensionsBundle/zipball/fa650e60e174afa06c09e28a54fb1854af04c7fe", + "reference": "fa650e60e174afa06c09e28a54fb1854af04c7fe", + "shasum": "" + }, + "require": { + "gedmo/doctrine-extensions": "^2.3.4 || ^3.0.0", + "php": "^7.1.3 || ^8.0", + "symfony/config": "^4.4 || ^5.2 || ^6.0", + "symfony/dependency-injection": "^4.4 || ^5.2 || ^6.0", + "symfony/event-dispatcher": "^4.4 || ^5.2 || ^6.0", + "symfony/http-kernel": "^4.4 || ^5.2 || ^6.0" + }, + "require-dev": { + "symfony/mime": "^4.4 || ^5.2 || ^6.0", + "symfony/phpunit-bridge": "^v5.2.4 || ^6.0", + "symfony/security-core": "^4.4 || ^5.2 || ^6.0" + }, + "suggest": { + "doctrine/doctrine-bundle": "to use the ORM extensions", + "doctrine/mongodb-odm-bundle": "to use the MongoDB ODM extensions", + "symfony/mime": "To use the Mime component integration for Uploadable" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Stof\\DoctrineExtensionsBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + } + ], + "description": "Integration of the gedmo/doctrine-extensions with Symfony", + "homepage": "https://github.com/stof/StofDoctrineExtensionsBundle", + "keywords": [ + "behaviors", + "doctrine2", + "extensions", + "gedmo", + "loggable", + "nestedset", + "sluggable", + "sortable", + "timestampable", + "translatable", + "tree" + ], + "support": { + "issues": "https://github.com/stof/StofDoctrineExtensionsBundle/issues", + "source": "https://github.com/stof/StofDoctrineExtensionsBundle/tree/v1.7.1" + }, + "time": "2022-09-30T11:52:24+00:00" + }, + { + "name": "symfony/asset", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/asset.git", + "reference": "223df790e684ecc7bc37323c2d1e265129ca02de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/asset/zipball/223df790e684ecc7bc37323c2d1e265129ca02de", + "reference": "223df790e684ecc7bc37323c2d1e265129ca02de", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "symfony/http-foundation": "<5.4" + }, + "require-dev": { + "symfony/http-client": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0" + }, + "suggest": { + "symfony/http-foundation": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Asset\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/asset/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-14T08:44:56+00:00" + }, + { + "name": "symfony/cache", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "1ce7ed8e7ca6948892b6a3a52bb60cf2b04f7c94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/1ce7ed8e7ca6948892b6a3a52bb60cf2b04f7c94", + "reference": "1ce7ed8e7ca6948892b6a3a52bb60cf2b04f7c94", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^1.1.7|^2|^3", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/var-exporter": "^6.2.10" + }, + "conflict": { + "doctrine/dbal": "<2.13.1", + "symfony/dependency-injection": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/var-dumper": "<5.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^2.13.1|^3.0", + "predis/predis": "^1.1", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-21T15:42:15+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "eeb71f04b6f7f34ca6d15633df82e014528b1632" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/eeb71f04b6f7f34ca6d15633df82e014528b1632", + "reference": "eeb71f04b6f7f34ca6d15633df82e014528b1632", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "suggest": { + "symfony/cache-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.3-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.2.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-01T10:32:47+00:00" + }, + { + "name": "symfony/clock", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "54d724dcda6298a05db32aa7f69be1ce638b8417" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/54d724dcda6298a05db32aa7f69be1ce638b8417", + "reference": "54d724dcda6298a05db32aa7f69be1ce638b8417", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-20T14:53:25+00:00" + }, + { + "name": "symfony/config", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "249271da6f545d6579e0663374f8249a80be2893" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/249271da6f545d6579e0663374f8249a80be2893", + "reference": "249271da6f545d6579e0663374f8249a80be2893", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/filesystem": "^5.4|^6.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<5.4" + }, + "require-dev": { + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/yaml": "^5.4|^6.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-14T08:44:56+00:00" + }, + { + "name": "symfony/console", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "12288d9f4500f84a4d02254d4aa968b15488476f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/12288d9f4500f84a4d02254d4aa968b15488476f", + "reference": "12288d9f4500f84a4d02254d4aa968b15488476f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.4|^6.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/lock": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-28T13:37:43+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "aedf3cb0f5b929ec255d96bbb4909e9932c769e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/aedf3cb0f5b929ec255d96bbb4909e9932c769e0", + "reference": "aedf3cb0f5b929ec255d96bbb4909e9932c769e0", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-14T08:44:56+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "d732a66a2672669232c0b4536c8c96724a679780" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/d732a66a2672669232c0b4536c8c96724a679780", + "reference": "d732a66a2672669232c0b4536c8c96724a679780", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/service-contracts": "^1.1.6|^2.0|^3.0", + "symfony/var-exporter": "^6.2.7" + }, + "conflict": { + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.1", + "symfony/finder": "<5.4", + "symfony/proxy-manager-bridge": "<6.2", + "symfony/yaml": "<5.4" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^6.1", + "symfony/expression-language": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" + }, + "suggest": { + "symfony/config": "", + "symfony/expression-language": "For using expressions in service container configuration", + "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-21T15:42:15+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", + "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.3-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.2.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-01T10:25:55+00:00" + }, + { + "name": "symfony/doctrine-bridge", + "version": "v6.2.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/doctrine-bridge.git", + "reference": "4b3aeaa90d41c5527d7ba211d12102cedf06936e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/4b3aeaa90d41c5527d7ba211d12102cedf06936e", + "reference": "4b3aeaa90d41c5527d7ba211d12102cedf06936e", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^1.2|^2", + "doctrine/persistence": "^2|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "conflict": { + "doctrine/dbal": "<2.13.1", + "doctrine/lexer": "<1.1", + "doctrine/orm": "<2.7.4", + "phpunit/phpunit": "<5.4.3", + "symfony/cache": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/form": "<5.4.21|>=6,<6.2.7", + "symfony/http-kernel": "<6.2", + "symfony/messenger": "<5.4", + "symfony/property-info": "<5.4", + "symfony/security-bundle": "<5.4", + "symfony/security-core": "<6.0", + "symfony/validator": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4|^2", + "doctrine/collections": "^1.0|^2.0", + "doctrine/data-fixtures": "^1.1", + "doctrine/dbal": "^2.13.1|^3.0", + "doctrine/orm": "^2.7.4", + "psr/log": "^1|^2|^3", + "symfony/cache": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/doctrine-messenger": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/form": "^5.4.21|^6.2.7", + "symfony/http-kernel": "^6.2", + "symfony/messenger": "^5.4|^6.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/proxy-manager-bridge": "^5.4|^6.0", + "symfony/security-core": "^6.0", + "symfony/stopwatch": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/uid": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "suggest": { + "doctrine/data-fixtures": "", + "doctrine/dbal": "", + "doctrine/orm": "", + "symfony/form": "", + "symfony/property-info": "", + "symfony/validator": "" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Doctrine\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Doctrine with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/doctrine-bridge/tree/v6.2.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-11T16:08:35+00:00" + }, + { + "name": "symfony/doctrine-messenger", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/doctrine-messenger.git", + "reference": "a1fdf9ce88df35d538907b52cd1dac2bf86ef507" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/doctrine-messenger/zipball/a1fdf9ce88df35d538907b52cd1dac2bf86ef507", + "reference": "a1fdf9ce88df35d538907b52cd1dac2bf86ef507", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^2.13|^3.0", + "php": ">=8.1", + "symfony/messenger": "^5.4|^6.0", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "conflict": { + "doctrine/persistence": "<1.3" + }, + "require-dev": { + "doctrine/persistence": "^1.3|^2|^3", + "symfony/property-access": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0" + }, + "type": "symfony-messenger-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\Bridge\\Doctrine\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Doctrine Messenger Bridge", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/doctrine-messenger/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-18T13:46:08+00:00" + }, + { + "name": "symfony/dotenv", + "version": "v6.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/dotenv.git", + "reference": "4481aa45be7a11d2335c1d5b5bbe2f0c6199b105" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/4481aa45be7a11d2335c1d5b5bbe2f0c6199b105", + "reference": "4481aa45be7a11d2335c1d5b5bbe2f0c6199b105", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "symfony/console": "<5.4", + "symfony/process": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Dotenv\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Registers environment variables from a .env file", + "homepage": "https://symfony.com", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "source": "https://github.com/symfony/dotenv/tree/v6.2.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-10T10:06:03+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "8b7e9f124640cb0611624a9383176c3e5f7d8cfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8b7e9f124640cb0611624a9383176c3e5f7d8cfb", + "reference": "8b7e9f124640cb0611624a9383176c3e5f7d8cfb", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^5.4|^6.0" + }, + "require-dev": { + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-18T13:46:08+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v6.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "04046f35fd7d72f9646e721fc2ecb8f9c67d3339" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/04046f35fd7d72f9646e721fc2ecb8f9c67d3339", + "reference": "04046f35fd7d72f9646e721fc2ecb8f9c67d3339", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^2|^3" + }, + "conflict": { + "symfony/dependency-injection": "<5.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/error-handler": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/stopwatch": "^5.4|^6.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v6.2.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-20T16:06:02+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "0ad3b6f1e4e2da5690fefe075cd53a238646d8dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/0ad3b6f1e4e2da5690fefe075cd53a238646d8dd", + "reference": "0ad3b6f1e4e2da5690fefe075cd53a238646d8dd", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "suggest": { + "symfony/event-dispatcher-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.3-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.2.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-01T10:32:47+00:00" + }, + { + "name": "symfony/expression-language", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/expression-language.git", + "reference": "83e1fee4c018aa60bcbbecd585a2c54af6aca905" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/83e1fee4c018aa60bcbbecd585a2c54af6aca905", + "reference": "83e1fee4c018aa60bcbbecd585a2c54af6aca905", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/cache": "^5.4|^6.0", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ExpressionLanguage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an engine that can compile and evaluate expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/expression-language/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-16T09:57:23+00:00" + }, + { + "name": "symfony/fake-sms-notifier", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/fake-sms-notifier.git", + "reference": "d6363378b5be5e77ee9238ea029a0cee8ebe42a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/fake-sms-notifier/zipball/d6363378b5be5e77ee9238ea029a0cee8ebe42a4", + "reference": "d6363378b5be5e77ee9238ea029a0cee8ebe42a4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/http-client": "^5.4|^6.0", + "symfony/mailer": "^5.4|^6.0", + "symfony/notifier": "^6.2.7" + }, + "type": "symfony-notifier-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Notifier\\Bridge\\FakeSms\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "James Hemery", + "homepage": "https://github.com/JamesHemery" + }, + { + "name": "Antoine Makdessi", + "email": "amakdessi@me.com", + "homepage": "http://antoine.makdessi.free.fr" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Fake SMS (as email or log during development) Notifier Bridge.", + "homepage": "https://symfony.com", + "keywords": [ + "development", + "email", + "notifier", + "sms", + "symfony" + ], + "support": { + "source": "https://github.com/symfony/fake-sms-notifier/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-17T11:05:34+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "fd588debf7d1bc16a2c84b4b3b71145d9946b894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/fd588debf7d1bc16a2c84b4b3b71145d9946b894", + "reference": "fd588debf7d1bc16a2c84b4b3b71145d9946b894", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-18T13:46:08+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "20808dc6631aecafbe67c186af5dcb370be3a0eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/20808dc6631aecafbe67c186af5dcb370be3a0eb", + "reference": "20808dc6631aecafbe67c186af5dcb370be3a0eb", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-16T09:57:23+00:00" + }, + { + "name": "symfony/flex", + "version": "v2.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/flex.git", + "reference": "2ff8465e7172790a47ab3c129f2b514eb2d8a286" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/flex/zipball/2ff8465e7172790a47ab3c129f2b514eb2d8a286", + "reference": "2ff8465e7172790a47ab3c129f2b514eb2d8a286", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.1", + "php": ">=8.0" + }, + "require-dev": { + "composer/composer": "^2.1", + "symfony/dotenv": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Flex\\Flex" + }, + "autoload": { + "psr-4": { + "Symfony\\Flex\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien.potencier@gmail.com" + } + ], + "description": "Composer plugin for Symfony", + "support": { + "issues": "https://github.com/symfony/flex/issues", + "source": "https://github.com/symfony/flex/tree/v2.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-18T08:03:15+00:00" + }, + { + "name": "symfony/form", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/form.git", + "reference": "a123512b46caea497ab8d96d9dbdbdaaf416a606" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/form/zipball/a123512b46caea497ab8d96d9dbdbdaaf416a606", + "reference": "a123512b46caea497ab8d96d9dbdbdaaf416a606", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/options-resolver": "^5.4|^6.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "conflict": { + "phpunit/phpunit": "<5.4.3", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/doctrine-bridge": "<5.4.21|>=6,<6.2.7", + "symfony/error-handler": "<5.4", + "symfony/framework-bundle": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/translation": "<5.4", + "symfony/translation-contracts": "<1.1.7", + "symfony/twig-bridge": "<5.4.21|>=6,<6.2.7" + }, + "require-dev": { + "doctrine/collections": "^1.0|^2.0", + "symfony/config": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/html-sanitizer": "^6.1", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/intl": "^5.4|^6.0", + "symfony/security-core": "^6.2", + "symfony/security-csrf": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/uid": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "suggest": { + "symfony/security-core": "For hashing users passwords.", + "symfony/security-csrf": "For protecting forms against CSRF attacks.", + "symfony/twig-bridge": "For templating with Twig.", + "symfony/validator": "For form validation." + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Form\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows to easily create, process and reuse HTML forms", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/form/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-19T08:03:37+00:00" + }, + { + "name": "symfony/framework-bundle", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/framework-bundle.git", + "reference": "823f285befde4e97bb70d97cae57997c38e4d6fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/823f285befde4e97bb70d97cae57997c38e4d6fd", + "reference": "823f285befde4e97bb70d97cae57997c38e4d6fd", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.1", + "symfony/cache": "^5.4|^6.0", + "symfony/config": "^6.1", + "symfony/dependency-injection": "^6.2.8", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/error-handler": "^6.1", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/http-foundation": "^6.2", + "symfony/http-kernel": "^6.2.1", + "symfony/polyfill-mbstring": "~1.0", + "symfony/routing": "^5.4|^6.0" + }, + "conflict": { + "doctrine/annotations": "<1.13.1", + "doctrine/persistence": "<1.3", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "phpunit/phpunit": "<5.4.3", + "symfony/asset": "<5.4", + "symfony/console": "<5.4", + "symfony/dom-crawler": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/form": "<5.4", + "symfony/http-client": "<5.4", + "symfony/lock": "<5.4", + "symfony/mailer": "<5.4", + "symfony/messenger": "<6.2", + "symfony/mime": "<6.2", + "symfony/property-access": "<5.4", + "symfony/property-info": "<5.4", + "symfony/security-core": "<5.4", + "symfony/security-csrf": "<5.4", + "symfony/serializer": "<6.1", + "symfony/stopwatch": "<5.4", + "symfony/translation": "<6.2.8", + "symfony/twig-bridge": "<5.4", + "symfony/twig-bundle": "<5.4", + "symfony/validator": "<5.4", + "symfony/web-profiler-bundle": "<5.4", + "symfony/workflow": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.13.1|^2", + "doctrine/persistence": "^1.3|^2|^3", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/asset": "^5.4|^6.0", + "symfony/browser-kit": "^5.4|^6.0", + "symfony/console": "^5.4.9|^6.0.9", + "symfony/css-selector": "^5.4|^6.0", + "symfony/dom-crawler": "^5.4|^6.0", + "symfony/dotenv": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/form": "^5.4|^6.0", + "symfony/html-sanitizer": "^6.1", + "symfony/http-client": "^5.4|^6.0", + "symfony/lock": "^5.4|^6.0", + "symfony/mailer": "^5.4|^6.0", + "symfony/messenger": "^6.2", + "symfony/mime": "^6.2", + "symfony/notifier": "^5.4|^6.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/process": "^5.4|^6.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/rate-limiter": "^5.4|^6.0", + "symfony/security-bundle": "^5.4|^6.0", + "symfony/semaphore": "^5.4|^6.0", + "symfony/serializer": "^6.1", + "symfony/stopwatch": "^5.4|^6.0", + "symfony/string": "^5.4|^6.0", + "symfony/translation": "^6.2.8", + "symfony/twig-bundle": "^5.4|^6.0", + "symfony/uid": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0", + "symfony/web-link": "^5.4|^6.0", + "symfony/workflow": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0", + "twig/twig": "^2.10|^3.0" + }, + "suggest": { + "ext-apcu": "For best performance of the system caches", + "symfony/console": "For using the console commands", + "symfony/form": "For using forms", + "symfony/property-info": "For using the property_info service", + "symfony/serializer": "For using the serializer service", + "symfony/validator": "For using validation", + "symfony/web-link": "For using web links, features such as preloading, prefetching or prerendering", + "symfony/yaml": "For using the debug:config and lint:yaml commands" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\FrameworkBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/framework-bundle/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-23T08:23:35+00:00" + }, + { + "name": "symfony/google-mailer", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/google-mailer.git", + "reference": "7e6cde8d40144e889e607bfc5320ea4192b247cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/google-mailer/zipball/7e6cde8d40144e889e607bfc5320ea4192b247cd", + "reference": "7e6cde8d40144e889e607bfc5320ea4192b247cd", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/mailer": "^5.4.21|^6.2.7" + }, + "require-dev": { + "symfony/http-client": "^5.4|^6.0" + }, + "type": "symfony-mailer-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\Bridge\\Google\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Google Mailer Bridge", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/google-mailer/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-14T16:23:31+00:00" + }, + { + "name": "symfony/html-sanitizer", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/html-sanitizer.git", + "reference": "211e36bbb20a5e5db2b940399ab1d2b421a08068" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/211e36bbb20a5e5db2b940399ab1d2b421a08068", + "reference": "211e36bbb20a5e5db2b940399ab1d2b421a08068", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "league/uri": "^6.5", + "masterminds/html5": "^2.7.2", + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HtmlSanitizer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Titouan Galopin", + "email": "galopintitouan@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to sanitize untrusted HTML input for safe insertion into a document's DOM.", + "homepage": "https://symfony.com", + "keywords": [ + "Purifier", + "html", + "sanitizer" + ], + "support": { + "source": "https://github.com/symfony/html-sanitizer/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-14T08:44:56+00:00" + }, + { + "name": "symfony/http-client", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "3f5545a91c8e79dedd1a06c4b04e1682c80c42f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/3f5545a91c8e79dedd1a06c4b04e1682c80c42f9", + "reference": "3f5545a91c8e79dedd1a06c4b04e1682c80c42f9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/http-client-contracts": "^3", + "symfony/service-contracts": "^1.0|^2|^3" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/amp": "^2.5", + "amphp/http-client": "^4.2.1", + "amphp/http-tunnel": "^1.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/stopwatch": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-20T13:12:48+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "df2ecd6cb70e73c1080e6478aea85f5f4da2c48b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/df2ecd6cb70e73c1080e6478aea85f5f4da2c48b", + "reference": "df2ecd6cb70e73c1080e6478aea85f5f4da2c48b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "suggest": { + "symfony/http-client-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.3-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.2.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-01T10:32:47+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "49adbb92bcb4e3c2943719d2756271e8b9602acc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/49adbb92bcb4e3c2943719d2756271e8b9602acc", + "reference": "49adbb92bcb4e3c2943719d2756271e8b9602acc", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.1" + }, + "conflict": { + "symfony/cache": "<6.2" + }, + "require-dev": { + "predis/predis": "~1.0", + "symfony/cache": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", + "symfony/mime": "^5.4|^6.0", + "symfony/rate-limiter": "^5.2|^6.0" + }, + "suggest": { + "symfony/mime": "To use the file extension guesser" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-18T13:46:08+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "81064a65a5496f17d2b6984f6519406f98864215" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/81064a65a5496f17d2b6984f6519406f98864215", + "reference": "81064a65a5496f17d2b6984f6519406f98864215", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/error-handler": "^6.1", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/http-foundation": "^5.4.21|^6.2.7", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<5.4", + "symfony/cache": "<5.4", + "symfony/config": "<6.1", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<6.2", + "symfony/doctrine-bridge": "<5.4", + "symfony/form": "<5.4", + "symfony/http-client": "<5.4", + "symfony/mailer": "<5.4", + "symfony/messenger": "<5.4", + "symfony/translation": "<5.4", + "symfony/twig-bridge": "<5.4", + "symfony/validator": "<5.4", + "twig/twig": "<2.13" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^5.4|^6.0", + "symfony/config": "^6.1", + "symfony/console": "^5.4|^6.0", + "symfony/css-selector": "^5.4|^6.0", + "symfony/dependency-injection": "^6.2", + "symfony/dom-crawler": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/http-client-contracts": "^1.1|^2|^3", + "symfony/process": "^5.4|^6.0", + "symfony/routing": "^5.4|^6.0", + "symfony/stopwatch": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/translation-contracts": "^1.1|^2|^3", + "symfony/uid": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "suggest": { + "symfony/browser-kit": "", + "symfony/config": "", + "symfony/console": "", + "symfony/dependency-injection": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-28T13:50:28+00:00" + }, + { + "name": "symfony/intl", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/intl.git", + "reference": "860c99e53149d22df1900d3aefdaeb17adb7669d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/intl/zipball/860c99e53149d22df1900d3aefdaeb17adb7669d", + "reference": "860c99e53149d22df1900d3aefdaeb17adb7669d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Intl\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Eriksen Costa", + "email": "eriksen.costa@infranology.com.br" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides access to the localization data of the ICU library", + "homepage": "https://symfony.com", + "keywords": [ + "i18n", + "icu", + "internationalization", + "intl", + "l10n", + "localization" + ], + "support": { + "source": "https://github.com/symfony/intl/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-14T16:23:31+00:00" + }, + { + "name": "symfony/mailer", + "version": "v6.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "bfcfa015c67e19c6fdb7ca6fe70700af1e740a17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/bfcfa015c67e19c6fdb7ca6fe70700af1e740a17", + "reference": "bfcfa015c67e19c6fdb7ca6fe70700af1e740a17", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.1", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/mime": "^6.2", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "conflict": { + "symfony/http-kernel": "<5.4", + "symfony/messenger": "<6.2", + "symfony/mime": "<6.2", + "symfony/twig-bridge": "<6.2.1" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/messenger": "^6.2", + "symfony/twig-bridge": "^6.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v6.2.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-14T15:00:05+00:00" + }, + { + "name": "symfony/mercure", + "version": "v0.6.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/mercure.git", + "reference": "af2953ae60e77628339659a31b8b2a9e40ca1dd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mercure/zipball/af2953ae60e77628339659a31b8b2a9e40ca1dd9", + "reference": "af2953ae60e77628339659a31b8b2a9e40ca1dd9", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/deprecation-contracts": "^2.0|^3.0", + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0", + "symfony/polyfill-php80": "^1.22", + "symfony/web-link": "^4.4|^5.0|^6.0" + }, + "require-dev": { + "lcobucci/jwt": "^3.4|^4.0|^5.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0", + "symfony/phpunit-bridge": "^5.2|^6.0", + "symfony/stopwatch": "^4.4|^5.0|^6.0", + "twig/twig": "^2.0|^3.0" + }, + "suggest": { + "symfony/stopwatch": "Integration with the profiler performances" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.5.x-dev" + }, + "thanks": { + "name": "dunglas/mercure", + "url": "https://github.com/dunglas/mercure" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Mercure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Mercure Component", + "homepage": "https://symfony.com", + "keywords": [ + "mercure", + "push", + "sse", + "updates" + ], + "support": { + "issues": "https://github.com/symfony/mercure/issues", + "source": "https://github.com/symfony/mercure/tree/v0.6.3" + }, + "funding": [ + { + "url": "https://github.com/dunglas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/mercure", + "type": "tidelift" + } + ], + "time": "2023-03-06T22:43:27+00:00" + }, + { + "name": "symfony/mercure-bundle", + "version": "v0.3.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/mercure-bundle.git", + "reference": "262c922912d94d01230b2c647208d06fc46d4cff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mercure-bundle/zipball/262c922912d94d01230b2c647208d06fc46d4cff", + "reference": "262c922912d94d01230b2c647208d06fc46d4cff", + "shasum": "" + }, + "require": { + "lcobucci/jwt": "^3.4|^4.0|^5.0", + "php": ">=7.1.3", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.4|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0", + "symfony/mercure": "^0.6.1", + "symfony/web-link": "^4.4|^5.0|^6.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.3.7|^5.0|^6.0", + "symfony/stopwatch": "^4.3.7|^5.0|^6.0", + "symfony/ux-turbo": "*", + "symfony/var-dumper": "^4.3.7|^5.0|^6.0" + }, + "suggest": { + "symfony/messenger": "To use the Messenger integration" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MercureBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony MercureBundle", + "homepage": "https://symfony.com", + "keywords": [ + "mercure", + "push", + "sse", + "updates" + ], + "support": { + "issues": "https://github.com/symfony/mercure-bundle/issues", + "source": "https://github.com/symfony/mercure-bundle/tree/v0.3.6" + }, + "funding": [ + { + "url": "https://github.com/dunglas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/mercure-bundle", + "type": "tidelift" + } + ], + "time": "2023-03-06T22:48:03+00:00" + }, + { + "name": "symfony/messenger", + "version": "v6.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/messenger.git", + "reference": "f54eef78d500309bbc51291f8a353b4b0afef8c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/messenger/zipball/f54eef78d500309bbc51291f8a353b4b0afef8c1", + "reference": "f54eef78d500309bbc51291f8a353b4b0afef8c1", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "symfony/event-dispatcher": "<5.4", + "symfony/event-dispatcher-contracts": "<2", + "symfony/framework-bundle": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/serializer": "<5.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/console": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/rate-limiter": "^5.4|^6.0", + "symfony/routing": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/stopwatch": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0" + }, + "suggest": { + "enqueue/messenger-adapter": "For using the php-enqueue library as a transport." + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Samuel Roze", + "email": "samuel.roze@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps applications send and receive messages to/from other applications or via message queues", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/messenger/tree/v6.2.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-14T15:00:05+00:00" + }, + { + "name": "symfony/mime", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "b6c137fc53a9f7c4c951cd3f362b3734c7a97723" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/b6c137fc53a9f7c4c951cd3f362b3734c7a97723", + "reference": "b6c137fc53a9f7c4c951cd3f362b3734c7a97723", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<5.4", + "symfony/serializer": "<6.2" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/serializer": "^6.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-19T09:54:16+00:00" + }, + { + "name": "symfony/monolog-bridge", + "version": "v6.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bridge.git", + "reference": "34700f2e5c7e9eae78f8e59fc02399dd8f110cae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/34700f2e5c7e9eae78f8e59fc02399dd8f110cae", + "reference": "34700f2e5c7e9eae78f8e59fc02399dd8f110cae", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.25.1|^2|^3", + "php": ">=8.1", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "conflict": { + "symfony/console": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/security-core": "<6.0" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/mailer": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0", + "symfony/security-core": "^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "suggest": { + "symfony/console": "For the possibility to show log messages in console commands depending on verbosity settings.", + "symfony/http-kernel": "For using the debugging handlers together with the response life cycle of the HTTP kernel.", + "symfony/var-dumper": "For using the debugging handlers like the console handler or the log server handler." + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Monolog\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Monolog with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/monolog-bridge/tree/v6.2.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-09T16:20:02+00:00" + }, + { + "name": "symfony/monolog-bundle", + "version": "v3.8.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "a41bbcdc1105603b6d73a7d9a43a3788f8e0fb7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/a41bbcdc1105603b6d73a7d9a43a3788f8e0fb7d", + "reference": "a41bbcdc1105603b6d73a7d9a43a3788f8e0fb7d", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.22 || ^2.0 || ^3.0", + "php": ">=7.1.3", + "symfony/config": "~4.4 || ^5.0 || ^6.0", + "symfony/dependency-injection": "^4.4 || ^5.0 || ^6.0", + "symfony/http-kernel": "~4.4 || ^5.0 || ^6.0", + "symfony/monolog-bridge": "~4.4 || ^5.0 || ^6.0" + }, + "require-dev": { + "symfony/console": "~4.4 || ^5.0 || ^6.0", + "symfony/phpunit-bridge": "^5.2 || ^6.0", + "symfony/yaml": "~4.4 || ^5.0 || ^6.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MonologBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony MonologBundle", + "homepage": "https://symfony.com", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/symfony/monolog-bundle/issues", + "source": "https://github.com/symfony/monolog-bundle/tree/v3.8.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-10T14:24:36+00:00" + }, + { + "name": "symfony/notifier", + "version": "v6.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/notifier.git", + "reference": "993df4464c577e7eb828324181d2a451e81619b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/notifier/zipball/993df4464c577e7eb828324181d2a451e81619b6", + "reference": "993df4464c577e7eb828324181d2a451e81619b6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "symfony/event-dispatcher": "<5.4", + "symfony/http-kernel": "<5.4" + }, + "require-dev": { + "symfony/event-dispatcher-contracts": "^2|^3", + "symfony/http-client-contracts": "^2|^3", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Notifier\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Sends notifications via one or more channels (email, SMS, ...)", + "homepage": "https://symfony.com", + "keywords": [ + "notification", + "notifier" + ], + "support": { + "source": "https://github.com/symfony/notifier/tree/v6.2.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-10T10:06:03+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "aa0e85b53bbb2b4951960efd61d295907eacd629" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/aa0e85b53bbb2b4951960efd61d295907eacd629", + "reference": "aa0e85b53bbb2b4951960efd61d295907eacd629", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-14T08:44:56+00:00" + }, + { + "name": "symfony/ovh-cloud-notifier", + "version": "v6.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/ovh-cloud-notifier.git", + "reference": "e52d74d630df94d2f2eaa581e2aa98100c894e43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ovh-cloud-notifier/zipball/e52d74d630df94d2f2eaa581e2aa98100c894e43", + "reference": "e52d74d630df94d2f2eaa581e2aa98100c894e43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/http-client": "^5.4|^6.0", + "symfony/notifier": "^6.2.7" + }, + "type": "symfony-notifier-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Notifier\\Bridge\\OvhCloud\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Thomas Ferney", + "email": "thomas.ferney@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony OvhCloud Notifier Bridge", + "homepage": "https://symfony.com", + "keywords": [ + "notifier", + "ovh-cloud", + "sms" + ], + "support": { + "source": "https://github.com/symfony/ovh-cloud-notifier/tree/v6.2.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-14T15:56:17+00:00" + }, + { + "name": "symfony/password-hasher", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/password-hasher.git", + "reference": "67820d8570bf1c2c2cd87cb76d9d12a9d52ab808" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/67820d8570bf1c2c2cd87cb76d9d12a9d52ab808", + "reference": "67820d8570bf1c2c2cd87cb76d9d12a9d52ab808", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "symfony/security-core": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0", + "symfony/security-core": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PasswordHasher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides password hashing utilities", + "homepage": "https://symfony.com", + "keywords": [ + "hashing", + "password" + ], + "support": { + "source": "https://github.com/symfony/password-hasher/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-14T08:44:56+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", + "reference": "511a08c03c1960e08a883f4cffcacd219b758354", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-intl-icu", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-icu.git", + "reference": "a3d9148e2c363588e05abbdd4ee4f971f0a5330c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/a3d9148e2c363588e05abbdd4ee4f971f0a5330c", + "reference": "a3d9148e2c363588e05abbdd4ee4f971f0a5330c", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance and support of other locales than \"en\"" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Icu\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's ICU-related data and classes", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "icu", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "639084e360537a19f9ee352433b84ce831f3d2da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/639084e360537a19f9ee352433b84ce831f3d2da", + "reference": "639084e360537a19f9ee352433b84ce831f3d2da", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "f3cf1a645c2734236ed1e2e671e273eeb3586166" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/f3cf1a645c2734236ed1e2e671e273eeb3586166", + "reference": "f3cf1a645c2734236ed1e2e671e273eeb3586166", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/property-access", + "version": "v6.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "2ad1e0a07b8cab3e09905659d14f3b248e916374" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/2ad1e0a07b8cab3e09905659d14f3b248e916374", + "reference": "2ad1e0a07b8cab3e09905659d14f3b248e916374", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/property-info": "^5.4|^6.0" + }, + "require-dev": { + "symfony/cache": "^5.4|^6.0" + }, + "suggest": { + "psr/cache-implementation": "To cache access methods." + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v6.2.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-14T15:00:05+00:00" + }, + { + "name": "symfony/property-info", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "617177c24e1a92e011851948ba973758429a68b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/617177c24e1a92e011851948ba973758429a68b2", + "reference": "617177c24e1a92e011851948ba973758429a68b2", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/string": "^5.4|^6.0" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/dependency-injection": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4|^2", + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpdoc-parser": "^1.0", + "symfony/cache": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0" + }, + "suggest": { + "phpdocumentor/reflection-docblock": "To use the PHPDoc", + "psr/cache-implementation": "To cache results", + "symfony/doctrine-bridge": "To use Doctrine metadata", + "symfony/serializer": "To use Serializer metadata" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-18T13:46:08+00:00" + }, + { + "name": "symfony/proxy-manager-bridge", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/proxy-manager-bridge.git", + "reference": "534119513ce1d06faa6d55b6717f237e980b4e91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/proxy-manager-bridge/zipball/534119513ce1d06faa6d55b6717f237e980b4e91", + "reference": "534119513ce1d06faa6d55b6717f237e980b4e91", + "shasum": "" + }, + "require": { + "friendsofphp/proxy-manager-lts": "^1.0.2", + "php": ">=8.1", + "symfony/dependency-injection": "^6.2" + }, + "require-dev": { + "symfony/config": "^6.1" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\ProxyManager\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for ProxyManager with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/proxy-manager-bridge/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-16T09:57:23+00:00" + }, + { + "name": "symfony/rate-limiter", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/rate-limiter.git", + "reference": "e82784d6a8d64a33f44a8acfd703327a32831533" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/e82784d6a8d64a33f44a8acfd703327a32831533", + "reference": "e82784d6a8d64a33f44a8acfd703327a32831533", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/options-resolver": "^5.4|^6.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/lock": "^5.4|^6.0" + }, + "suggest": { + "symfony/lock": "For preventing race conditions in rate limiters" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\RateLimiter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Wouter de Jong", + "email": "wouter@wouterj.nl" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a Token Bucket implementation to rate limit input and output in your application", + "homepage": "https://symfony.com", + "keywords": [ + "limiter", + "rate-limiter" + ], + "support": { + "source": "https://github.com/symfony/rate-limiter/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-24T10:42:00+00:00" + }, + { + "name": "symfony/requirements-checker", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/requirements-checker.git", + "reference": "cf8893f384348a338157d637e170fe8fb2356016" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/requirements-checker/zipball/cf8893f384348a338157d637e170fe8fb2356016", + "reference": "cf8893f384348a338157d637e170fe8fb2356016", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "bin": [ + "bin/requirements-checker" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Requirements\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Check Symfony requirements and give recommendations", + "keywords": [ + "configuration", + "distribution" + ], + "support": { + "issues": "https://github.com/symfony/requirements-checker/issues", + "source": "https://github.com/symfony/requirements-checker/tree/v2.0.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-30T16:18:33+00:00" + }, + { + "name": "symfony/routing", + "version": "v6.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "69062e2823f03b82265d73a966999660f0e1e404" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/69062e2823f03b82265d73a966999660f0e1e404", + "reference": "69062e2823f03b82265d73a966999660f0e1e404", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "symfony/config": "<6.2", + "symfony/dependency-injection": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.2", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" + }, + "suggest": { + "symfony/config": "For using the all-in-one router or any loader", + "symfony/expression-language": "For using expression matching", + "symfony/http-foundation": "For using a Symfony Request object", + "symfony/yaml": "For using the YAML loader" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v6.2.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-14T15:00:05+00:00" + }, + { + "name": "symfony/runtime", + "version": "v6.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/runtime.git", + "reference": "f8b0751b33888329be8f8f0481bb81d279ec4157" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/runtime/zipball/f8b0751b33888329be8f8f0481bb81d279ec4157", + "reference": "f8b0751b33888329be8f8f0481bb81d279ec4157", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": ">=8.1" + }, + "conflict": { + "symfony/dotenv": "<5.4" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "symfony/console": "^5.4|^6.0", + "symfony/dotenv": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Component\\Runtime\\Internal\\ComposerPlugin" + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Runtime\\": "", + "Symfony\\Runtime\\Symfony\\Component\\": "Internal/" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Enables decoupling PHP applications from global state", + "homepage": "https://symfony.com", + "keywords": [ + "runtime" + ], + "support": { + "source": "https://github.com/symfony/runtime/tree/v6.2.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-14T15:48:35+00:00" + }, + { + "name": "symfony/security-bundle", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-bundle.git", + "reference": "b12dcedbcf423ae6d34d79cfaa6791a21c90bd14" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/b12dcedbcf423ae6d34d79cfaa6791a21c90bd14", + "reference": "b12dcedbcf423ae6d34d79cfaa6791a21c90bd14", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.1", + "symfony/config": "^6.1", + "symfony/dependency-injection": "^6.2", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/http-foundation": "^6.2", + "symfony/http-kernel": "^6.2", + "symfony/password-hasher": "^5.4|^6.0", + "symfony/security-core": "^6.2", + "symfony/security-csrf": "^5.4|^6.0", + "symfony/security-http": "^6.2.10" + }, + "conflict": { + "symfony/browser-kit": "<5.4", + "symfony/console": "<5.4", + "symfony/framework-bundle": "<5.4", + "symfony/ldap": "<5.4", + "symfony/twig-bundle": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4|^2", + "symfony/asset": "^5.4|^6.0", + "symfony/browser-kit": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/css-selector": "^5.4|^6.0", + "symfony/dom-crawler": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/form": "^5.4|^6.0", + "symfony/framework-bundle": "^5.4|^6.0", + "symfony/ldap": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/rate-limiter": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/twig-bridge": "^5.4|^6.0", + "symfony/twig-bundle": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\SecurityBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-bundle/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-21T15:49:06+00:00" + }, + { + "name": "symfony/security-core", + "version": "v6.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-core.git", + "reference": "c141337bc7451f9a9e464733f1e536bf38d1d2fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-core/zipball/c141337bc7451f9a9e464733f1e536bf38d1d2fb", + "reference": "c141337bc7451f9a9e464733f1e536bf38d1d2fb", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^1.1|^2|^3", + "symfony/password-hasher": "^5.4|^6.0", + "symfony/service-contracts": "^1.1.6|^2|^3" + }, + "conflict": { + "symfony/event-dispatcher": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/ldap": "<5.4", + "symfony/security-guard": "<5.4", + "symfony/validator": "<5.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "psr/container": "^1.1|^2.0", + "psr/log": "^1|^2|^3", + "symfony/cache": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/ldap": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0" + }, + "suggest": { + "psr/container-implementation": "To instantiate the Security class", + "symfony/event-dispatcher": "", + "symfony/expression-language": "For using the expression voter", + "symfony/http-foundation": "", + "symfony/ldap": "For using LDAP integration", + "symfony/validator": "For using the user password constraint" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Core\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - Core Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-core/tree/v6.2.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-10T10:06:03+00:00" + }, + { + "name": "symfony/security-csrf", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-csrf.git", + "reference": "6cce7efdce68e0670d2f19acebc21dcd0798e333" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/6cce7efdce68e0670d2f19acebc21dcd0798e333", + "reference": "6cce7efdce68e0670d2f19acebc21dcd0798e333", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/security-core": "^5.4|^6.0" + }, + "conflict": { + "symfony/http-foundation": "<5.4" + }, + "require-dev": { + "symfony/http-foundation": "^5.4|^6.0" + }, + "suggest": { + "symfony/http-foundation": "For using the class SessionTokenStorage." + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Csrf\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - CSRF Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-csrf/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-16T09:57:23+00:00" + }, + { + "name": "symfony/security-http", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-http.git", + "reference": "c468f059fac27680acf7e84cea07ba5ffff8942a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-http/zipball/c468f059fac27680acf7e84cea07ba5ffff8942a", + "reference": "c468f059fac27680acf7e84cea07ba5ffff8942a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^6.2", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/security-core": "~6.0.19|~6.1.11|^6.2.5" + }, + "conflict": { + "symfony/event-dispatcher": "<5.4.9|>=6,<6.0.9", + "symfony/security-bundle": "<5.4", + "symfony/security-csrf": "<5.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/cache": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/rate-limiter": "^5.4|^6.0", + "symfony/routing": "^5.4|^6.0", + "symfony/security-csrf": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0" + }, + "suggest": { + "symfony/routing": "For using the HttpUtils class to create sub-requests, redirect the user, and match URLs", + "symfony/security-csrf": "For using tokens to protect authentication/logout attempts" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Http\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - HTTP Integration", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-http/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-21T11:56:14+00:00" + }, + { + "name": "symfony/serializer", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "0732edf0ad28dd3faacde4f1200ab9d7a4d5f40d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/0732edf0ad28dd3faacde4f1200ab9d7a4d5f40d", + "reference": "0732edf0ad28dd3faacde4f1200ab9d7a4d5f40d", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/dependency-injection": "<5.4", + "symfony/property-access": "<5.4", + "symfony/property-info": "<5.4", + "symfony/uid": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", + "symfony/cache": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/error-handler": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/form": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/uid": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0", + "symfony/var-exporter": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" + }, + "suggest": { + "psr/cache-implementation": "For using the metadata cache.", + "symfony/config": "For using the XML mapping loader.", + "symfony/mime": "For using a MIME type guesser within the DataUriNormalizer.", + "symfony/property-access": "For using the ObjectNormalizer.", + "symfony/property-info": "To deserialize relations.", + "symfony/var-exporter": "For using the metadata compiler.", + "symfony/yaml": "For using the default YAML mapping loader." + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/serializer/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-18T13:57:49+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "a8c9cedf55f314f3a186041d19537303766df09a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a8c9cedf55f314f3a186041d19537303766df09a", + "reference": "a8c9cedf55f314f3a186041d19537303766df09a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.3-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.2.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-01T10:32:47+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "f3adc98c1061875dd2edcd45e5b04e63d0e29f8f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/f3adc98c1061875dd2edcd45e5b04e63d0e29f8f", + "reference": "f3adc98c1061875dd2edcd45e5b04e63d0e29f8f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/service-contracts": "^1|^2|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-14T08:44:56+00:00" + }, + { + "name": "symfony/string", + "version": "v6.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "193e83bbd6617d6b2151c37fff10fa7168ebddef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/193e83bbd6617d6b2151c37fff10fa7168ebddef", + "reference": "193e83bbd6617d6b2151c37fff10fa7168ebddef", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.0" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/intl": "^6.2", + "symfony/translation-contracts": "^2.0|^3.0", + "symfony/var-exporter": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.2.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-20T16:06:02+00:00" + }, + { + "name": "symfony/translation", + "version": "v6.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "817535dbb1721df8b3a8f2489dc7e50bcd6209b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/817535dbb1721df8b3a8f2489dc7e50bcd6209b5", + "reference": "817535dbb1721df8b3a8f2489dc7e50bcd6209b5", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.3|^3.0" + }, + "conflict": { + "symfony/config": "<5.4", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/twig-bundle": "<5.4", + "symfony/yaml": "<5.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^4.13", + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/http-client-contracts": "^1.1|^2.0|^3.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/intl": "^5.4|^6.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^5.4|^6.0", + "symfony/service-contracts": "^1.1.2|^2|^3", + "symfony/yaml": "^5.4|^6.0" + }, + "suggest": { + "nikic/php-parser": "To use PhpAstExtractor", + "psr/log-implementation": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v6.2.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-31T09:14:44+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "dfec258b9dd17a6b24420d464c43bffe347441c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/dfec258b9dd17a6b24420d464c43bffe347441c8", + "reference": "dfec258b9dd17a6b24420d464c43bffe347441c8", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "suggest": { + "symfony/translation-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.3-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.2.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-01T10:32:47+00:00" + }, + { + "name": "symfony/twig-bridge", + "version": "v6.2.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bridge.git", + "reference": "30e3ad6ae749b2d2700ecf9b4a1a9d5c96b18927" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/30e3ad6ae749b2d2700ecf9b4a1a9d5c96b18927", + "reference": "30e3ad6ae749b2d2700ecf9b4a1a9d5c96b18927", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/translation-contracts": "^1.1|^2|^3", + "twig/twig": "^2.13|^3.0.4" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/console": "<5.4", + "symfony/form": "<6.2.7", + "symfony/http-foundation": "<5.4", + "symfony/http-kernel": "<6.2", + "symfony/mime": "<6.2", + "symfony/translation": "<5.4", + "symfony/workflow": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "egulias/email-validator": "^2.1.10|^3|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/asset": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/form": "^6.2.7", + "symfony/html-sanitizer": "^6.1", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^6.2", + "symfony/intl": "^5.4|^6.0", + "symfony/mime": "^6.2", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/routing": "^5.4|^6.0", + "symfony/security-acl": "^2.8|^3.0", + "symfony/security-core": "^5.4|^6.0", + "symfony/security-csrf": "^5.4|^6.0", + "symfony/security-http": "^5.4|^6.0", + "symfony/serializer": "^6.2", + "symfony/stopwatch": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/web-link": "^5.4|^6.0", + "symfony/workflow": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0", + "twig/cssinliner-extra": "^2.12|^3", + "twig/inky-extra": "^2.12|^3", + "twig/markdown-extra": "^2.12|^3" + }, + "suggest": { + "symfony/asset": "For using the AssetExtension", + "symfony/expression-language": "For using the ExpressionExtension", + "symfony/finder": "", + "symfony/form": "For using the FormExtension", + "symfony/html-sanitizer": "For using the HtmlSanitizerExtension", + "symfony/http-kernel": "For using the HttpKernelExtension", + "symfony/routing": "For using the RoutingExtension", + "symfony/security-core": "For using the SecurityExtension", + "symfony/security-csrf": "For using the CsrfExtension", + "symfony/security-http": "For using the LogoutUrlExtension", + "symfony/stopwatch": "For using the StopwatchExtension", + "symfony/translation": "For using the TranslationExtension", + "symfony/var-dumper": "For using the DumpExtension", + "symfony/web-link": "For using the WebLinkExtension", + "symfony/yaml": "For using the YamlExtension" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Twig\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Twig with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/twig-bridge/tree/v6.2.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-03-31T09:14:44+00:00" + }, + { + "name": "symfony/twig-bundle", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bundle.git", + "reference": "8bb562655c6ae4b8fae9cf72077591f38b961566" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/8bb562655c6ae4b8fae9cf72077591f38b961566", + "reference": "8bb562655c6ae4b8fae9cf72077591f38b961566", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "php": ">=8.1", + "symfony/config": "^6.1", + "symfony/dependency-injection": "^6.1", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^6.2", + "symfony/twig-bridge": "^6.2", + "twig/twig": "^2.13|^3.0.4" + }, + "conflict": { + "symfony/framework-bundle": "<5.4", + "symfony/translation": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4|^2", + "symfony/asset": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/form": "^5.4|^6.0", + "symfony/framework-bundle": "^5.4|^6.0", + "symfony/routing": "^5.4|^6.0", + "symfony/stopwatch": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/web-link": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\TwigBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of Twig into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/twig-bundle/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-14T08:44:56+00:00" + }, + { + "name": "symfony/twilio-notifier", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/twilio-notifier.git", + "reference": "0903cb2a31d01975c5e419a0459d40e8a785f646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twilio-notifier/zipball/0903cb2a31d01975c5e419a0459d40e8a785f646", + "reference": "0903cb2a31d01975c5e419a0459d40e8a785f646", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/http-client": "^5.4|^6.0", + "symfony/notifier": "^6.2.7" + }, + "type": "symfony-notifier-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Notifier\\Bridge\\Twilio\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Twilio Notifier Bridge", + "homepage": "https://symfony.com", + "keywords": [ + "notifier", + "sms", + "twilio" + ], + "support": { + "source": "https://github.com/symfony/twilio-notifier/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-17T11:05:34+00:00" + }, + { + "name": "symfony/uid", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "d30c72a63897cfa043e1de4d4dd2ffa9ecefcdc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/d30c72a63897cfa043e1de4d4dd2ffa9ecefcdc0", + "reference": "d30c72a63897cfa043e1de4d4dd2ffa9ecefcdc0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-14T08:44:56+00:00" + }, + { + "name": "symfony/ux-autocomplete", + "version": "v2.8.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/ux-autocomplete.git", + "reference": "b226b472aa693ad8e30cea330aff1bf0ccf62af8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ux-autocomplete/zipball/b226b472aa693ad8e30cea330aff1bf0ccf62af8", + "reference": "b226b472aa693ad8e30cea330aff1bf0ccf62af8", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/string": "^5.4|^6.0" + }, + "conflict": { + "doctrine/orm": "2.9.0 || 2.9.1" + }, + "require-dev": { + "doctrine/doctrine-bundle": "^2.4", + "doctrine/orm": "^2.9", + "mtdowling/jmespath.php": "2.6.x-dev", + "symfony/form": "^5.4|^6.0", + "symfony/framework-bundle": "^5.4|^6.0", + "symfony/maker-bundle": "^1.40", + "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/security-bundle": "^5.4|^6.0", + "symfony/security-csrf": "^5.4|^6.0", + "symfony/twig-bundle": "^5.4|^6.0", + "symfony/uid": "^5.4|^6.0", + "zenstruck/browser": "^1.1", + "zenstruck/foundry": "^1.19" + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "name": "symfony/ux", + "url": "https://github.com/symfony/ux" + } + }, + "autoload": { + "psr-4": { + "Symfony\\UX\\Autocomplete\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "JavaScript Autocomplete functionality for Symfony", + "homepage": "https://symfony.com", + "keywords": [ + "symfony-ux" + ], + "support": { + "source": "https://github.com/symfony/ux-autocomplete/tree/v2.8.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-24T18:32:24+00:00" + }, + { + "name": "symfony/validator", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "c02ea86844926f04247bc1f5db5f85bb53330823" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/c02ea86844926f04247bc1f5db5f85bb53330823", + "reference": "c02ea86844926f04247bc1f5db5f85bb53330823", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^1.1|^2|^3" + }, + "conflict": { + "doctrine/annotations": "<1.13", + "doctrine/lexer": "<1.1", + "phpunit/phpunit": "<5.4.3", + "symfony/dependency-injection": "<5.4", + "symfony/expression-language": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/intl": "<5.4", + "symfony/property-info": "<5.4", + "symfony/translation": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.13|^2", + "egulias/email-validator": "^2.1.10|^3|^4", + "symfony/cache": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/intl": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" + }, + "suggest": { + "egulias/email-validator": "Strict (RFC compliant) email validation", + "psr/cache-implementation": "For using the mapping cache.", + "symfony/config": "", + "symfony/expression-language": "For using the Expression validator and the ExpressionLanguageSyntax constraints", + "symfony/http-foundation": "", + "symfony/intl": "", + "symfony/property-access": "For accessing properties within comparison constraints", + "symfony/property-info": "To automatically add NotNull and Type constraints", + "symfony/translation": "For translating validation errors.", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to validate values", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/validator/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-19T09:54:16+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "41a750a23412ca76fdbbf5096943b4134272c1ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41a750a23412ca76fdbbf5096943b4134272c1ab", + "reference": "41a750a23412ca76fdbbf5096943b4134272c1ab", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "phpunit/phpunit": "<5.4.3", + "symfony/console": "<5.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/uid": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "suggest": { + "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", + "ext-intl": "To show region name in time zone dump", + "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-18T13:46:08+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "9a07920c2058bafee921ce4d90aeef2193837d63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/9a07920c2058bafee921ce4d90aeef2193837d63", + "reference": "9a07920c2058bafee921ce4d90aeef2193837d63", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/var-dumper": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-21T08:33:05+00:00" + }, + { + "name": "symfony/web-link", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/web-link.git", + "reference": "410aac2034608ac661cdca1968e3c56d4164abc8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/web-link/zipball/410aac2034608ac661cdca1968e3c56d4164abc8", + "reference": "410aac2034608ac661cdca1968e3c56d4164abc8", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/link": "^1.1|^2.0" + }, + "conflict": { + "symfony/http-kernel": "<5.4" + }, + "provide": { + "psr/link-implementation": "1.0|2.0" + }, + "require-dev": { + "symfony/http-kernel": "^5.4|^6.0" + }, + "suggest": { + "symfony/http-kernel": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\WebLink\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Manages links between resources", + "homepage": "https://symfony.com", + "keywords": [ + "dns-prefetch", + "http", + "http2", + "link", + "performance", + "prefetch", + "preload", + "prerender", + "psr13", + "push" + ], + "support": { + "source": "https://github.com/symfony/web-link/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-14T08:44:56+00:00" + }, + { + "name": "symfony/webpack-encore-bundle", + "version": "v1.16.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/webpack-encore-bundle.git", + "reference": "1862d71e483769b40278548a30e756ce13ef9d4c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/1862d71e483769b40278548a30e756ce13ef9d4c", + "reference": "1862d71e483769b40278548a30e756ce13ef9d4c", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/asset": "^4.4 || ^5.0 || ^6.0", + "symfony/config": "^4.4 || ^5.0 || ^6.0", + "symfony/dependency-injection": "^4.4 || ^5.0 || ^6.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/http-kernel": "^4.4 || ^5.0 || ^6.0", + "symfony/polyfill-php80": "^1.25.0", + "symfony/service-contracts": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "symfony/framework-bundle": "^4.4 || ^5.0 || ^6.0", + "symfony/phpunit-bridge": "^5.3 || ^6.0", + "symfony/twig-bundle": "^4.4 || ^5.0 || ^6.0", + "symfony/web-link": "^4.4 || ^5.0 || ^6.0" + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "name": "symfony/webpack-encore", + "url": "https://github.com/symfony/webpack-encore" + } + }, + "autoload": { + "psr-4": { + "Symfony\\WebpackEncoreBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Integration with your Symfony app & Webpack Encore!", + "support": { + "issues": "https://github.com/symfony/webpack-encore-bundle/issues", + "source": "https://github.com/symfony/webpack-encore-bundle/tree/v1.16.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-18T19:37:55+00:00" + }, + { + "name": "symfony/workflow", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/workflow.git", + "reference": "d3b73e52ccd2b3682a612d2718ba7123fa37e3d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/workflow/zipball/d3b73e52ccd2b3682a612d2718ba7123fa37e3d2", + "reference": "d3b73e52ccd2b3682a612d2718ba7123fa37e3d2", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "symfony/event-dispatcher": "<5.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/security-core": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Workflow\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools for managing a workflow or finite state machine", + "homepage": "https://symfony.com", + "keywords": [ + "petrinet", + "place", + "state", + "statemachine", + "transition", + "workflow" + ], + "support": { + "source": "https://github.com/symfony/workflow/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-24T10:42:00+00:00" + }, + { + "name": "symfony/yaml", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "61916f3861b1e9705b18cfde723921a71dd1559d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/61916f3861b1e9705b18cfde723921a71dd1559d", + "reference": "61916f3861b1e9705b18cfde723921a71dd1559d", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-28T13:25:36+00:00" + }, + { + "name": "theofidry/alice-data-fixtures", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/AliceDataFixtures.git", + "reference": "798ea2b5b8c6b2b938470d5330dc4beee77f4aff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/AliceDataFixtures/zipball/798ea2b5b8c6b2b938470d5330dc4beee77f4aff", + "reference": "798ea2b5b8c6b2b938470d5330dc4beee77f4aff", + "shasum": "" + }, + "require": { + "nelmio/alice": "^3.10", + "php": "^8.1", + "psr/log": "^1 || ^2 || ^3", + "webmozart/assert": "^1.10" + }, + "conflict": { + "doctrine/orm": "<2.6.3", + "doctrine/persistence": "<2.0", + "illuminate/database": "<8.12", + "ocramius/proxy-manager": "<2.1", + "symfony/framework-bundle": "<5.4", + "zendframework/zend-code": "<3.3.1" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/annotations": "^1.13", + "phpspec/prophecy": "^1.14.0", + "phpspec/prophecy-phpunit": "^2.0.1", + "phpunit/phpunit": "^9.5.10", + "symfony/phpunit-bridge": "^5.3.8 || ^6.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "To use Doctrine with the MongoDB flavour", + "doctrine/data-fixtures": "To use Doctrine", + "doctrine/dbal": "To use Doctrine with the PHPCR flavour", + "doctrine/mongodb": "To use Doctrine with the MongoDB flavour", + "doctrine/mongodb-odm": "To use Doctrine with the MongoDB flavour", + "doctrine/orm": "To use Doctrine ORM", + "doctrine/phpcr-odm": "To use Doctrine with the PHPCR flavour", + "illuminate/database": "To use Eloquent", + "jackalope/jackalope-doctrine-dbal": "To use Doctrine with the PHPCR flavour", + "ocramius/proxy-manager": "To avoid database connection on kernel boot" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fidry\\AliceDataFixtures\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com", + "homepage": "https://github.com/theofidry" + } + ], + "description": "Nelmio alice extension to persist the loaded fixtures.", + "keywords": [ + "Fixture", + "alice", + "data", + "faker", + "orm", + "tests" + ], + "support": { + "issues": "https://github.com/theofidry/AliceDataFixtures/issues", + "source": "https://github.com/theofidry/AliceDataFixtures/tree/1.6.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2022-07-03T13:30:00+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "2.2.6", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "c42125b83a4fa63b187fdf29f9c93cb7733da30c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/c42125b83a4fa63b187fdf29f9c93cb7733da30c", + "reference": "c42125b83a4fa63b187fdf29f9c93cb7733da30c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^5.5 || ^7.0 || ^8.0", + "symfony/css-selector": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^7.5 || ^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/2.2.6" + }, + "time": "2023-01-03T09:29:04+00:00" + }, + { + "name": "twig/cssinliner-extra", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/cssinliner-extra.git", + "reference": "85c8f3d7712bab57f6162f9637613df0511f207b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/cssinliner-extra/zipball/85c8f3d7712bab57f6162f9637613df0511f207b", + "reference": "85c8f3d7712bab57f6162f9637613df0511f207b", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "tijsverkoyen/css-to-inline-styles": "^2.0", + "twig/twig": "^2.7|^3.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Twig\\Extra\\CssInliner\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Twig extension to allow inlining CSS", + "homepage": "https://twig.symfony.com", + "keywords": [ + "css", + "inlining", + "twig" + ], + "support": { + "source": "https://github.com/twigphp/cssinliner-extra/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2023-02-09T06:45:16+00:00" + }, + { + "name": "twig/extra-bundle", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/twig-extra-bundle.git", + "reference": "4a9674e775f49a9df5e26da66546e8f3364afe67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/4a9674e775f49a9df5e26da66546e8f3364afe67", + "reference": "4a9674e775f49a9df5e26da66546e8f3364afe67", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/framework-bundle": "^4.4|^5.0|^6.0", + "symfony/twig-bundle": "^4.4|^5.0|^6.0", + "twig/twig": "^2.7|^3.0" + }, + "require-dev": { + "league/commonmark": "^1.0|^2.0", + "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0", + "twig/cache-extra": "^3.0", + "twig/cssinliner-extra": "^2.12|^3.0", + "twig/html-extra": "^2.12|^3.0", + "twig/inky-extra": "^2.12|^3.0", + "twig/intl-extra": "^2.12|^3.0", + "twig/markdown-extra": "^2.12|^3.0", + "twig/string-extra": "^2.12|^3.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Twig\\Extra\\TwigExtraBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Symfony bundle for extra Twig extensions", + "homepage": "https://twig.symfony.com", + "keywords": [ + "bundle", + "extra", + "twig" + ], + "support": { + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2023-04-14T11:03:02+00:00" + }, + { + "name": "twig/inky-extra", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/inky-extra.git", + "reference": "907abf7046082cc151a3ee01f268dbf5f5f28eab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/inky-extra/zipball/907abf7046082cc151a3ee01f268dbf5f5f28eab", + "reference": "907abf7046082cc151a3ee01f268dbf5f5f28eab", + "shasum": "" + }, + "require": { + "lorenzo/pinky": "^1.0.5", + "php": ">=7.1.3", + "twig/twig": "^2.7|^3.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Twig\\Extra\\Inky\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Twig extension for the inky email templating engine", + "homepage": "https://twig.symfony.com", + "keywords": [ + "email", + "emails", + "inky", + "twig" + ], + "support": { + "source": "https://github.com/twigphp/inky-extra/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2023-02-09T06:45:16+00:00" + }, + { + "name": "twig/intl-extra", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/intl-extra.git", + "reference": "a97c323bebfca009d02994a5a8568c0b412a49ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/a97c323bebfca009d02994a5a8568c0b412a49ab", + "reference": "a97c323bebfca009d02994a5a8568c0b412a49ab", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/intl": "^4.4|^5.0|^6.0", + "twig/twig": "^2.7|^3.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Twig\\Extra\\Intl\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Twig extension for Intl", + "homepage": "https://twig.symfony.com", + "keywords": [ + "intl", + "twig" + ], + "support": { + "source": "https://github.com/twigphp/intl-extra/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2023-02-09T06:45:16+00:00" + }, + { + "name": "twig/markdown-extra", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/markdown-extra.git", + "reference": "8f1179e279cea6ef14066a4560b859df58acd5d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/8f1179e279cea6ef14066a4560b859df58acd5d8", + "reference": "8f1179e279cea6ef14066a4560b859df58acd5d8", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "twig/twig": "^2.7|^3.0" + }, + "require-dev": { + "erusev/parsedown": "^1.7", + "league/commonmark": "^1.0|^2.0", + "league/html-to-markdown": "^4.8|^5.0", + "michelf/php-markdown": "^1.8|^2.0", + "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Twig\\Extra\\Markdown\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Twig extension for Markdown", + "homepage": "https://twig.symfony.com", + "keywords": [ + "html", + "markdown", + "twig" + ], + "support": { + "source": "https://github.com/twigphp/markdown-extra/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2023-02-09T06:45:16+00:00" + }, + { + "name": "twig/twig", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "106c170d08e8415d78be2d16c3d057d0d108262b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/106c170d08e8415d78be2d16c3d057d0d108262b", + "reference": "106c170d08e8415d78be2d16c3d057d0d108262b", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2023-05-03T19:06:57+00:00" + }, + { + "name": "webbaard/payum-mollie", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/coopTilleuls/payum-mollie.git", + "reference": "ee778a349d0302a34b1d9800e8284c09fb78b94c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/coopTilleuls/payum-mollie/zipball/ee778a349d0302a34b1d9800e8284c09fb78b94c", + "reference": "ee778a349d0302a34b1d9800e8284c09fb78b94c", + "shasum": "" + }, + "require": { + "mollie/mollie-api-php": "^2.44", + "payum/core": "^1.5", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "payum/core": "^1.3@dev", + "php-http/guzzle6-adapter": "^1.0", + "phpspec/phpspec": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "PayHelper\\Payum\\Mollie\\": "" + } + }, + "autoload-dev": { + "psr-4": { + "PayHelper\\Payum\\Mollie\\spec\\": "spec/" + } + }, + "license": [ + "GPLv3" + ], + "authors": [ + { + "name": "Tim Huijzers", + "email": "webbaard@gmail.com" + } + ], + "description": "The Payum extension. It provides Mollie payment integration. used to be maintained by Rafał Muszyński but couldn't see any activity", + "homepage": "https://sourcefabric.org", + "keywords": [ + "creditcard", + "directdebit", + "mollie", + "payment", + "payum", + "sepa" + ], + "support": { + "source": "https://github.com/coopTilleuls/payum-mollie/tree/1.0.5", + "issues": "https://github.com/coopTilleuls/payum-mollie/issues" + }, + "time": "2023-05-16T08:11:42+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + }, + { + "name": "willdurand/geocoder", + "version": "4.6.0", + "source": { + "type": "git", + "url": "https://github.com/geocoder-php/php-common.git", + "reference": "be3d9ed0fddf8c698ee079d8a07ae9520b4a49a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/geocoder-php/php-common/zipball/be3d9ed0fddf8c698ee079d8a07ae9520b4a49a1", + "reference": "be3d9ed0fddf8c698ee079d8a07ae9520b4a49a1", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "nyholm/nsa": "^1.1", + "phpunit/phpunit": "^9.5", + "symfony/stopwatch": "~2.5" + }, + "suggest": { + "symfony/stopwatch": "If you want to use the TimedGeocoder" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Geocoder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "William Durand", + "email": "william.durand1@gmail.com" + } + ], + "description": "Common files for PHP Geocoder", + "homepage": "http://geocoder-php.org", + "keywords": [ + "abstraction", + "geocoder", + "geocoding", + "geoip" + ], + "support": { + "source": "https://github.com/geocoder-php/php-common/tree/4.6.0" + }, + "time": "2022-07-30T11:09:43+00:00" + }, + { + "name": "willdurand/geocoder-bundle", + "version": "5.19.0", + "source": { + "type": "git", + "url": "https://github.com/geocoder-php/BazingaGeocoderBundle.git", + "reference": "f4530ab17c9453f1308f1f89ad736300f34455a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/geocoder-php/BazingaGeocoderBundle/zipball/f4530ab17c9453f1308f1f89ad736300f34455a1", + "reference": "f4530ab17c9453f1308f1f89ad736300f34455a1", + "shasum": "" + }, + "require": { + "geocoder-php/plugin": "^1.5", + "php": "^7.4 || ^8.0", + "php-http/discovery": "^1.14", + "symfony/console": "^4.4 || ^5.0 || ^6.0", + "symfony/framework-bundle": "^4.4 || ^5.0 || ^6.0", + "symfony/options-resolver": "^4.4 || ^5.0 || ^6.0", + "willdurand/geocoder": "^4.6" + }, + "conflict": { + "geocoder-php/nominatim-provider": "<5.0" + }, + "require-dev": { + "doctrine/orm": "~2.8", + "fakerphp/faker": "^1.20", + "friendsofphp/php-cs-fixer": "^3.13", + "geocoder-php/algolia-places-provider": "^0.4", + "geocoder-php/arcgis-online-provider": "^4.4", + "geocoder-php/bing-maps-provider": "^4.3", + "geocoder-php/cache-provider": "^4.4.0", + "geocoder-php/chain-provider": "^4.5", + "geocoder-php/free-geoip-provider": "^4.5", + "geocoder-php/geo-plugin-provider": "^4.3", + "geocoder-php/geoip2-provider": "^4.3", + "geocoder-php/geoips-provider": "^4.0", + "geocoder-php/geonames-provider": "^4.4", + "geocoder-php/google-maps-places-provider": "^1.4", + "geocoder-php/google-maps-provider": "^4.7", + "geocoder-php/here-provider": "^0.7", + "geocoder-php/host-ip-provider": "^4.4", + "geocoder-php/ip-info-db-provider": "^4.3", + "geocoder-php/ip-info-provider": "^0.4", + "geocoder-php/ipstack-provider": "^0.4", + "geocoder-php/locationiq-provider": "^1.4", + "geocoder-php/mapbox-provider": "^1.4", + "geocoder-php/mapquest-provider": "^4.3", + "geocoder-php/mapzen-provider": "^4.0", + "geocoder-php/maxmind-binary-provider": "^4.3", + "geocoder-php/maxmind-provider": "^4.4", + "geocoder-php/nominatim-provider": "^5.6", + "geocoder-php/open-cage-provider": "^4.6", + "geocoder-php/openrouteservice-provider": "^1.3", + "geocoder-php/pickpoint-provider": "^4.3", + "geocoder-php/tomtom-provider": "^4.4", + "geocoder-php/yandex-provider": "^4.5", + "geoip/geoip": "~1.17", + "nyholm/nsa": "^1.3", + "nyholm/psr7": "^1.5", + "nyholm/symfony-bundle-test": "dev-master", + "php-http/curl-client": "^2.2", + "php-http/message": "^1.13", + "phpstan/phpstan": "^1.9.2", + "symfony/config": "^4.4 || ^5.0 || ^6.0", + "symfony/phpunit-bridge": "^5.2 || ^6.0", + "symfony/validator": "^4.4 || ^5.0 || ^6.0", + "symfony/yaml": "^4.4 || ^5.0 || ^6.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "Bazinga\\GeocoderBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "William Durand", + "email": "will+git@drnd.me" + } + ], + "description": "Integration of Geocoder into Symfony", + "keywords": [ + "geocoder", + "geocoding" + ], + "support": { + "issues": "https://github.com/geocoder-php/BazingaGeocoderBundle/issues", + "source": "https://github.com/geocoder-php/BazingaGeocoderBundle/tree/5.19.0" + }, + "time": "2022-12-05T13:56:11+00:00" + }, + { + "name": "willdurand/negotiation", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/willdurand/Negotiation.git", + "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/willdurand/Negotiation/zipball/68e9ea0553ef6e2ee8db5c1d98829f111e623ec2", + "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Negotiation\\": "src/Negotiation" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "William Durand", + "email": "will+git@drnd.me" + } + ], + "description": "Content Negotiation tools for PHP provided as a standalone library.", + "homepage": "http://williamdurand.fr/Negotiation/", + "keywords": [ + "accept", + "content", + "format", + "header", + "negotiation" + ], + "support": { + "issues": "https://github.com/willdurand/Negotiation/issues", + "source": "https://github.com/willdurand/Negotiation/tree/3.1.0" + }, + "time": "2022-01-30T20:08:53+00:00" + } + ], + "packages-dev": [ + { + "name": "composer/pcre", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", + "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.1.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-11-17T09:50:14+00:00" + }, + { + "name": "composer/semver", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-04-01T19:23:25+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "ced299686f41dce890debac69273b47ffe98a40c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-25T21:32:43+00:00" + }, + { + "name": "ekino/phpstan-banned-code", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/ekino/phpstan-banned-code.git", + "reference": "4f0d7c8a0c9f5d222ffc24234aa6c5b3b71bf4c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ekino/phpstan-banned-code/zipball/4f0d7c8a0c9f5d222ffc24234aa6c5b3b71bf4c3", + "reference": "4f0d7c8a0c9f5d222ffc24234aa6c5b3b71bf4c3", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0", + "phpstan/phpstan": "^1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.6", + "friendsofphp/php-cs-fixer": "^3.0", + "nikic/php-parser": "^4.3", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/var-dumper": "^5.0" + }, + "type": "phpstan-extension", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Ekino\\PHPStanBannedCode\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rémi Marseille", + "email": "remi.marseille@ekino.com", + "homepage": "https://www.ekino.com" + } + ], + "description": "Detected banned code using PHPStan", + "homepage": "https://github.com/ekino/phpstan-banned-code", + "keywords": [ + "PHPStan", + "code quality" + ], + "support": { + "issues": "https://github.com/ekino/phpstan-banned-code/issues", + "source": "https://github.com/ekino/phpstan-banned-code/tree/v1.0.0" + }, + "time": "2021-11-02T08:37:34+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.14.2", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "14f0541651841b63640e7aafad041ad55dc7aa88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/14f0541651841b63640e7aafad041ad55dc7aa88", + "reference": "14f0541651841b63640e7aafad041ad55dc7aa88", + "shasum": "" + }, + "require": { + "composer/semver": "^3.3", + "composer/xdebug-handler": "^3.0.3", + "doctrine/annotations": "^1.14.2 || ^2", + "doctrine/lexer": "^2", + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0", + "sebastian/diff": "^4.0", + "symfony/console": "^5.4 || ^6.0", + "symfony/event-dispatcher": "^5.4 || ^6.0", + "symfony/filesystem": "^5.4 || ^6.0", + "symfony/finder": "^5.4 || ^6.0", + "symfony/options-resolver": "^5.4 || ^6.0", + "symfony/polyfill-mbstring": "^1.27", + "symfony/polyfill-php80": "^1.27", + "symfony/polyfill-php81": "^1.27", + "symfony/process": "^5.4 || ^6.0", + "symfony/stopwatch": "^5.4 || ^6.0" + }, + "require-dev": { + "justinrainbow/json-schema": "^5.2", + "keradus/cli-executor": "^2.0", + "mikey179/vfsstream": "^1.6.11", + "php-coveralls/php-coveralls": "^2.5.3", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", + "phpspec/prophecy": "^1.16", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "phpunitgoodpractices/polyfill": "^1.6", + "phpunitgoodpractices/traits": "^1.9.2", + "symfony/phpunit-bridge": "^6.2.3", + "symfony/yaml": "^5.4 || ^6.0" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.14.2" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2023-01-29T23:47:01+00:00" + }, + { + "name": "friendsoftwig/twigcs", + "version": "6.2.0", + "source": { + "type": "git", + "url": "https://github.com/friendsoftwig/twigcs.git", + "reference": "da697cc1bf6bf22feb1d8dbefcbdb1451e6a35f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/friendsoftwig/twigcs/zipball/da697cc1bf6bf22feb1d8dbefcbdb1451e6a35f6", + "reference": "da697cc1bf6bf22feb1d8dbefcbdb1451e6a35f6", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-hash": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0", + "symfony/console": "^4.4 || ^5.3 || ^6.0", + "symfony/filesystem": "^4.4 || ^5.3 || ^6.0", + "symfony/finder": "^4.4 || ^5.3 || ^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5.27", + "symfony/phpunit-bridge": "^6.2.3" + }, + "bin": [ + "bin/twigcs" + ], + "type": "library", + "autoload": { + "psr-4": { + "FriendsOfTwig\\Twigcs\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tristan Maindron", + "email": "tmaindron@gmail.com" + } + ], + "description": "Checkstyle automation for Twig", + "support": { + "issues": "https://github.com/friendsoftwig/twigcs/issues", + "source": "https://github.com/friendsoftwig/twigcs/tree/6.2.0" + }, + "time": "2023-01-13T16:02:00+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.15.4", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" + }, + "time": "2023-03-05T19:49:14+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, + "time": "2021-07-20T11:28:43+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "php-webdriver/webdriver", + "version": "1.14.0", + "source": { + "type": "git", + "url": "https://github.com/php-webdriver/php-webdriver.git", + "reference": "3ea4f924afb43056bf9c630509e657d951608563" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/3ea4f924afb43056bf9c630509e657d951608563", + "reference": "3ea4f924afb43056bf9c630509e657d951608563", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-zip": "*", + "php": "^7.3 || ^8.0", + "symfony/polyfill-mbstring": "^1.12", + "symfony/process": "^5.0 || ^6.0" + }, + "replace": { + "facebook/webdriver": "*" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.20.0", + "ondram/ci-detector": "^4.0", + "php-coveralls/php-coveralls": "^2.4", + "php-mock/php-mock-phpunit": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpunit/phpunit": "^9.3", + "squizlabs/php_codesniffer": "^3.5", + "symfony/var-dumper": "^5.0 || ^6.0" + }, + "suggest": { + "ext-SimpleXML": "For Firefox profile creation" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Exception/TimeoutException.php" + ], + "psr-4": { + "Facebook\\WebDriver\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.", + "homepage": "https://github.com/php-webdriver/php-webdriver", + "keywords": [ + "Chromedriver", + "geckodriver", + "php", + "selenium", + "webdriver" + ], + "support": { + "issues": "https://github.com/php-webdriver/php-webdriver/issues", + "source": "https://github.com/php-webdriver/php-webdriver/tree/1.14.0" + }, + "time": "2023-02-09T12:12:19+00:00" + }, + { + "name": "phpstan/extension-installer", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/extension-installer.git", + "reference": "f5e02d40f277d28513001976f444d9ff1dc15e9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/f5e02d40f277d28513001976f444d9ff1dc15e9a", + "reference": "f5e02d40f277d28513001976f444d9ff1dc15e9a", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.8.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2.0", + "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPStan\\ExtensionInstaller\\Plugin", + "phpstan/extension-installer": { + "ignore": [ + "phpstan/phpstan-phpunit" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\ExtensionInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Composer plugin for automatic installation of PHPStan extensions", + "support": { + "issues": "https://github.com/phpstan/extension-installer/issues", + "source": "https://github.com/phpstan/extension-installer/tree/1.3.0" + }, + "time": "2023-04-18T13:08:02+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.10.15", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "762c4dac4da6f8756eebb80e528c3a47855da9bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/762c4dac4da6f8756eebb80e528c3a47855da9bd", + "reference": "762c4dac4da6f8756eebb80e528c3a47855da9bd", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2023-05-09T15:28:01+00:00" + }, + { + "name": "phpstan/phpstan-deprecation-rules", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", + "reference": "a22b36b955a2e9a3d39fe533b6c1bb5359f9c319" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/a22b36b955a2e9a3d39fe533b6c1bb5359f9c319", + "reference": "a22b36b955a2e9a3d39fe533b6c1bb5359f9c319", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.10" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-php-parser": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "support": { + "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.1.3" + }, + "time": "2023-03-17T07:50:08+00:00" + }, + { + "name": "phpstan/phpstan-doctrine", + "version": "1.3.40", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-doctrine.git", + "reference": "f741919a720af6f84249abc62befeb15eee7bc88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/f741919a720af6f84249abc62befeb15eee7bc88", + "reference": "f741919a720af6f84249abc62befeb15eee7bc88", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.10.12" + }, + "conflict": { + "doctrine/collections": "<1.0", + "doctrine/common": "<2.7", + "doctrine/mongodb-odm": "<1.2", + "doctrine/orm": "<2.5", + "doctrine/persistence": "<1.3" + }, + "require-dev": { + "composer/semver": "^3.3.2", + "doctrine/annotations": "^1.11.0", + "doctrine/collections": "^1.6", + "doctrine/common": "^2.7 || ^3.0", + "doctrine/dbal": "^2.13.8 || ^3.3.3", + "doctrine/lexer": "^1.2.1", + "doctrine/mongodb-odm": "^1.3 || ^2.1", + "doctrine/orm": "^2.11.0", + "doctrine/persistence": "^1.3.8 || ^2.2.1", + "gedmo/doctrine-extensions": "^3.8", + "nesbot/carbon": "^2.49", + "nikic/php-parser": "^4.13.2", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5.10", + "ramsey/uuid-doctrine": "^1.5.0", + "symfony/cache": "^4.4.35" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Doctrine extensions for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-doctrine/issues", + "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.40" + }, + "time": "2023-05-11T11:26:04+00:00" + }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "b21c03d4f6f3a446e4311155f4be9d65048218e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/b21c03d4f6f3a446e4311155f4be9d65048218e6", + "reference": "b21c03d4f6f3a446e4311155f4be9d65048218e6", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.10" + }, + "require-dev": { + "nikic/php-parser": "^4.13.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.5.1" + }, + "time": "2023-03-29T14:47:40+00:00" + }, + { + "name": "phpstan/phpstan-symfony", + "version": "1.3.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-symfony.git", + "reference": "7332b90dfc291ac5b4b83fbca2081936faa1e3f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/7332b90dfc291ac5b4b83fbca2081936faa1e3f9", + "reference": "7332b90dfc291ac5b4b83fbca2081936faa1e3f9", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.9.18" + }, + "conflict": { + "symfony/framework-bundle": "<3.0" + }, + "require-dev": { + "nikic/php-parser": "^4.13.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^8.5.29 || ^9.5", + "psr/container": "1.0 || 1.1.1", + "symfony/config": "^5.4 || ^6.1", + "symfony/console": "^5.4 || ^6.1", + "symfony/dependency-injection": "^5.4 || ^6.1", + "symfony/form": "^5.4 || ^6.1", + "symfony/framework-bundle": "^5.4 || ^6.1", + "symfony/http-foundation": "^5.4 || ^6.1", + "symfony/messenger": "^5.4", + "symfony/polyfill-php80": "^1.24", + "symfony/serializer": "^5.4" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lukáš Unger", + "email": "looky.msc@gmail.com", + "homepage": "https://lookyman.net" + } + ], + "description": "Symfony Framework extensions and rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-symfony/issues", + "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.2" + }, + "time": "2023-05-16T12:46:15+00:00" + }, + { + "name": "phpstan/phpstan-webmozart-assert", + "version": "1.2.4", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-webmozart-assert.git", + "reference": "d1ff28697bd4e1c9ef5d3f871367ce9092871fec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-webmozart-assert/zipball/d1ff28697bd4e1c9ef5d3f871367ce9092871fec", + "reference": "d1ff28697bd4e1c9ef5d3f871367ce9092871fec", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.10" + }, + "require-dev": { + "nikic/php-parser": "^4.13.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "webmozart/assert": "^1.11.0" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan webmozart/assert extension", + "support": { + "issues": "https://github.com/phpstan/phpstan-webmozart-assert/issues", + "source": "https://github.com/phpstan/phpstan-webmozart-assert/tree/1.2.4" + }, + "time": "2023-02-21T20:34:19+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.26", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.15", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-03-06T12:58:08+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "17d621b3aff84d0c8b62539e269e87d8d5baa76e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/17d621b3aff84d0c8b62539e269e87d8d5baa76e", + "reference": "17d621b3aff84d0c8b62539e269e87d8d5baa76e", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.8" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2023-05-11T05:14:45+00:00" + }, + { + "name": "rector/rector", + "version": "0.14.8", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "46ee9a173a2b2645ca92a75ffc17460139fa226e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/46ee9a173a2b2645ca92a75ffc17460139fa226e", + "reference": "46ee9a173a2b2645ca92a75ffc17460139fa226e", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0", + "phpstan/phpstan": "^1.9.0" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-php-parser": "*", + "rector/rector-phpoffice": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.14-dev" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/0.14.8" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2022-11-14T14:09:49+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-02-14T08:28:10+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:45:17+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "staabm/annotate-pull-request-from-checkstyle", + "version": "1.8.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/annotate-pull-request-from-checkstyle.git", + "reference": "082e7f859860f6e79094b6ec86606bd6d0fe9014" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/annotate-pull-request-from-checkstyle/zipball/082e7f859860f6e79094b6ec86606bd6d0fe9014", + "reference": "082e7f859860f6e79094b6ec86606bd6d0fe9014", + "shasum": "" + }, + "require": { + "ext-libxml": "*", + "ext-simplexml": "*", + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16.1" + }, + "bin": [ + "cs2pr" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Markus Staab" + } + ], + "keywords": [ + "Github Actions", + "continous integration", + "dev" + ], + "support": { + "issues": "https://github.com/staabm/annotate-pull-request-from-checkstyle/issues", + "source": "https://github.com/staabm/annotate-pull-request-from-checkstyle/tree/1.8.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2023-05-08T15:56:45+00:00" + }, + { + "name": "symfony/browser-kit", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "87bd43240e6cc855f70ea1c7a448ab3bd442633c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/87bd43240e6cc855f70ea1c7a448ab3bd442633c", + "reference": "87bd43240e6cc855f70ea1c7a448ab3bd442633c", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/dom-crawler": "^5.4|^6.0" + }, + "require-dev": { + "symfony/css-selector": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0" + }, + "suggest": { + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/browser-kit/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-14T08:44:56+00:00" + }, + { + "name": "symfony/debug-bundle", + "version": "v6.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug-bundle.git", + "reference": "8ff6c96d09c462beade7512137899e400c76d994" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/8ff6c96d09c462beade7512137899e400c76d994", + "reference": "8ff6c96d09c462beade7512137899e400c76d994", + "shasum": "" + }, + "require": { + "ext-xml": "*", + "php": ">=8.1", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/twig-bridge": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "conflict": { + "symfony/config": "<5.4", + "symfony/dependency-injection": "<5.4" + }, + "require-dev": { + "symfony/config": "^5.4|^6.0", + "symfony/web-profiler-bundle": "^5.4|^6.0" + }, + "suggest": { + "symfony/config": "For service container configuration", + "symfony/dependency-injection": "For using as a service from the container" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\DebugBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of the Symfony VarDumper component and the ServerLogCommand from MonologBridge into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/debug-bundle/tree/v6.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-02-14T08:44:56+00:00" + }, + { + "name": "symfony/dom-crawler", + "version": "v6.2.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "328bc3795059651d2d4e462e8febdf7ec2d7a626" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/328bc3795059651d2d4e462e8febdf7ec2d7a626", + "reference": "328bc3795059651d2d4e462e8febdf7ec2d7a626", + "shasum": "" + }, + "require": { + "masterminds/html5": "^2.6", + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "^5.4|^6.0" + }, + "suggest": { + "symfony/css-selector": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases DOM navigation for HTML and XML documents", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dom-crawler/tree/v6.2.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-11T16:03:19+00:00" + }, + { + "name": "symfony/maker-bundle", + "version": "v1.48.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/maker-bundle.git", + "reference": "2e428e8432e9879187672fe08f1cc335e2a31dd6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/2e428e8432e9879187672fe08f1cc335e2a31dd6", + "reference": "2e428e8432e9879187672fe08f1cc335e2a31dd6", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "nikic/php-parser": "^4.11", + "php": ">=8.0", + "symfony/config": "^5.4.7|^6.0", + "symfony/console": "^5.4.7|^6.0", + "symfony/dependency-injection": "^5.4.7|^6.0", + "symfony/deprecation-contracts": "^2.2|^3", + "symfony/filesystem": "^5.4.7|^6.0", + "symfony/finder": "^5.4.3|^6.0", + "symfony/framework-bundle": "^5.4.7|^6.0", + "symfony/http-kernel": "^5.4.7|^6.0" + }, + "conflict": { + "doctrine/doctrine-bundle": "<2.4", + "doctrine/orm": "<2.10", + "symfony/doctrine-bridge": "<5.4" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/doctrine-bundle": "^2.4", + "doctrine/orm": "^2.10.0", + "symfony/http-client": "^5.4.7|^6.0", + "symfony/phpunit-bridge": "^5.4.7|^6.0", + "symfony/polyfill-php80": "^1.16.0", + "symfony/process": "^5.4.7|^6.0", + "symfony/security-core": "^5.4.7|^6.0", + "symfony/yaml": "^5.4.3|^6.0", + "twig/twig": "^2.0|^3.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MakerBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", + "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", + "keywords": [ + "code generator", + "generator", + "scaffold", + "scaffolding" + ], + "support": { + "issues": "https://github.com/symfony/maker-bundle/issues", + "source": "https://github.com/symfony/maker-bundle/tree/v1.48.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-14T10:48:46+00:00" + }, + { + "name": "symfony/panther", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/panther.git", + "reference": "dc572828ee81051b87c39db8a9be1bb78980d738" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/panther/zipball/dc572828ee81051b87c39db8a9be1bb78980d738", + "reference": "dc572828ee81051b87c39db8a9be1bb78980d738", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": ">=8.0", + "php-webdriver/webdriver": "^1.8.2", + "symfony/browser-kit": "^5.3 || ^6.0", + "symfony/dependency-injection": "^5.3 || ^6.0", + "symfony/deprecation-contracts": "^2.4 || ^3", + "symfony/dom-crawler": "^5.3 || ^6.0", + "symfony/http-client": "^5.3 || ^6.0", + "symfony/http-kernel": "^5.3 || ^6.0", + "symfony/process": "^5.3 || ^6.0" + }, + "require-dev": { + "symfony/css-selector": "^5.3 || ^6.0", + "symfony/framework-bundle": "^5.3 || ^6.0", + "symfony/mime": "^5.3 || ^6.0", + "symfony/phpunit-bridge": "^5.3 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Panther\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com", + "homepage": "https://dunglas.fr" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A browser testing and web scraping library for PHP and Symfony.", + "homepage": "https://dunglas.fr", + "keywords": [ + "e2e", + "scraping", + "selenium", + "symfony", + "testing", + "webdriver" + ], + "support": { + "issues": "https://github.com/symfony/panther/issues", + "source": "https://github.com/symfony/panther/tree/v2.0.1" + }, + "funding": [ + { + "url": "https://www.panthera.org/donate", + "type": "custom" + }, + { + "url": "https://github.com/dunglas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/panther", + "type": "tidelift" + } + ], + "time": "2021-12-02T17:32:12+00:00" + }, + { + "name": "symfony/phpunit-bridge", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/phpunit-bridge.git", + "reference": "552950db2919421ad917e29e76d1999a2a31a8e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/552950db2919421ad917e29e76d1999a2a31a8e3", + "reference": "552950db2919421ad917e29e76d1999a2a31a8e3", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "conflict": { + "phpunit/phpunit": "<7.5|9.1.2" + }, + "require-dev": { + "symfony/deprecation-contracts": "^2.1|^3.0", + "symfony/error-handler": "^5.4|^6.0" + }, + "suggest": { + "symfony/error-handler": "For tracking deprecated interfaces usages at runtime with DebugClassLoader" + }, + "bin": [ + "bin/simple-phpunit" + ], + "type": "symfony-bridge", + "extra": { + "thanks": { + "name": "phpunit/phpunit", + "url": "https://github.com/sebastianbergmann/phpunit" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Bridge\\PhpUnit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides utilities for PHPUnit, especially user deprecation notices management", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/phpunit-bridge/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-18T13:46:08+00:00" + }, + { + "name": "symfony/process", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "b34cdbc9c5e75d45a3703e63a48ad07aafa8bf2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/b34cdbc9c5e75d45a3703e63a48ad07aafa8bf2e", + "reference": "b34cdbc9c5e75d45a3703e63a48ad07aafa8bf2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-18T13:56:57+00:00" + }, + { + "name": "symfony/web-profiler-bundle", + "version": "v6.2.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/web-profiler-bundle.git", + "reference": "24b6f4370f1cd59aacfc5e799c8614b40776e9c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/24b6f4370f1cd59aacfc5e799c8614b40776e9c8", + "reference": "24b6f4370f1cd59aacfc5e799c8614b40776e9c8", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/config": "^5.4|^6.0", + "symfony/framework-bundle": "^5.4|^6.0", + "symfony/http-kernel": "^6.1", + "symfony/routing": "^5.4|^6.0", + "symfony/twig-bundle": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "conflict": { + "symfony/form": "<5.4", + "symfony/mailer": "<5.4", + "symfony/messenger": "<5.4" + }, + "require-dev": { + "symfony/browser-kit": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/css-selector": "^5.4|^6.0", + "symfony/stopwatch": "^5.4|^6.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\WebProfilerBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a development tool that gives detailed information about the execution of any request", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/web-profiler-bundle/tree/v6.2.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-04-24T13:41:17+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" + }, + { + "name": "zenstruck/assert", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/zenstruck/assert.git", + "reference": "8fdd1f1b23f3c8612176d78616b52aa60df1be7f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zenstruck/assert/zipball/8fdd1f1b23f3c8612176d78616b52aa60df1be7f", + "reference": "8fdd1f1b23f3c8612176d78616b52aa60df1be7f", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "symfony/polyfill-php81": "^1.23", + "symfony/var-exporter": "^5.4|^6.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9.5", + "symfony/phpunit-bridge": "^6.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Zenstruck\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Bond", + "email": "kevinbond@gmail.com" + } + ], + "description": "Standalone, lightweight, framework agnostic, test assertion library.", + "homepage": "https://github.com/zenstruck/assert", + "keywords": [ + "assertion", + "phpunit", + "test" + ], + "support": { + "issues": "https://github.com/zenstruck/assert/issues", + "source": "https://github.com/zenstruck/assert/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://github.com/kbond", + "type": "github" + } + ], + "time": "2023-04-17T15:45:16+00:00" + }, + { + "name": "zenstruck/messenger-test", + "version": "v1.7.2", + "source": { + "type": "git", + "url": "https://github.com/zenstruck/messenger-test.git", + "reference": "8ef7993cf452182ee843a614921552ae560e408c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zenstruck/messenger-test/zipball/8ef7993cf452182ee843a614921552ae560e408c", + "reference": "8ef7993cf452182ee843a614921552ae560e408c", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "symfony/deprecation-contracts": "^2.2|^3.0", + "symfony/framework-bundle": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0", + "zenstruck/assert": "^1.0" + }, + "conflict": { + "symfony/framework-bundle": "5.4.5|6.0.5" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9.5.0", + "symfony/browser-kit": "^5.4|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Zenstruck\\Messenger\\Test\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Bond", + "email": "kevinbond@gmail.com" + } + ], + "description": "Assertions and helpers for testing your symfony/messenger queues.", + "homepage": "https://github.com/zenstruck/messenger-test", + "keywords": [ + "Messenger", + "queue", + "symfony", + "test" + ], + "support": { + "issues": "https://github.com/zenstruck/messenger-test/issues", + "source": "https://github.com/zenstruck/messenger-test/tree/v1.7.2" + }, + "funding": [ + { + "url": "https://github.com/kbond", + "type": "github" + } + ], + "time": "2023-02-24T20:54:57+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=8.1.11", + "ext-apcu": "*", + "ext-ctype": "*", + "ext-iconv": "*", + "ext-intl": "*", + "ext-xsl": "*", + "ext-zip": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/composer.md b/composer.md new file mode 100644 index 0000000..428cbce --- /dev/null +++ b/composer.md @@ -0,0 +1,23 @@ +# Note regarding composer.json + +## Meilisearch + + meilisearch/meilisearch-php + +Is locked to `v0.26.0` to prevent this error: + + Executing script cache:clear [KO] + [KO] + Script cache:clear returned with error code 1 + !! + !! In DebugClassLoader.php line 327: + !! + !! Case mismatch between loaded and declared class names: "MeiliSearch\Client" + !! vs "Meilisearch\Client". + !! + !! + !! + Script @auto-scripts was called via post-update-cmd + + +I have opened a ticket https://github.com/meilisearch/meilisearch-php/issues/452. diff --git a/config/bundles.php b/config/bundles.php new file mode 100644 index 0000000..efd16ea --- /dev/null +++ b/config/bundles.php @@ -0,0 +1,36 @@ + ['all' => true], + Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], + Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], + Bazinga\GeocoderBundle\BazingaGeocoderBundle::class => ['all' => true], + Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle::class => ['all' => true], + Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle::class => ['all' => true], + Hautelook\AliceBundle\HautelookAliceBundle::class => ['all' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], + Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], + ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], + Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true], + Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true], + Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], + EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true], + Craue\FormFlowBundle\CraueFormFlowBundle::class => ['all' => true], + Zenstruck\Messenger\Test\ZenstruckMessengerTestBundle::class => ['dev' => true, 'test' => true], + Snc\RedisBundle\SncRedisBundle::class => ['all' => true], + Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], + League\FlysystemBundle\FlysystemBundle::class => ['all' => true], + Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true], + Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], + Symfony\UX\Autocomplete\AutocompleteBundle::class => ['all' => true], + Payum\Bundle\PayumBundle\PayumBundle::class => ['all' => true], + FOS\CKEditorBundle\FOSCKEditorBundle::class => ['all' => true], + Misd\PhoneNumberBundle\MisdPhoneNumberBundle::class => ['all' => true], +]; diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml new file mode 100644 index 0000000..2f79c6a --- /dev/null +++ b/config/packages/api_platform.yaml @@ -0,0 +1,24 @@ +api_platform: + title: 'PlateformCoop API' + description: 'List of PlateformCoop API endpoints. Most of them require to be logged.' + + # The version of the API. + version: '%app_version%' + + openapi: + # The contact information for the exposed API. + contact: + # The identifying name of the contact person/organization. + name: APES + # The URL pointing to the contact information. MUST be in the format of a URL. + url: http://www.apes-hdf.org/page-96-0-0.html + # The email address of the contact person/organization. MUST be in the format of an email address. + email: contact@apes-hdf.org + # A URL to the Terms of Service for the API. MUST be in the format of a URL. + termsOfService: https://github.com/ApesHDF/EBS/blob/main/LICENSE + # The license information for the exposed API. + license: + # The license name used for the API. + name: MIT + # URL to the license used for the API. MUST be in the format of a URL. + url: https://github.com/ApesHDF/EBS/blob/main/LICENSE diff --git a/config/packages/bazinga_geocoder.yaml b/config/packages/bazinga_geocoder.yaml new file mode 100644 index 0000000..3416159 --- /dev/null +++ b/config/packages/bazinga_geocoder.yaml @@ -0,0 +1,24 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + # we need a PSR16 cache for bazinga_geoloc + cache.geoloc.psr16: + class: Symfony\Component\Cache\Psr16Cache + arguments: ['@cache.geoloc'] + +# See the docs at https://github.com/geocoder-php/BazingaGeocoderBundle +bazinga_geocoder: + # The local IP (127.0.0.1) will be replaced by the fake_ip + # see https://github.com/geocoder-php/BazingaGeocoderBundle/blob/5.0.0/Resources/doc/index.md#fake-local-ip + fake_ip: 92.159.11.105 # Lomme + providers: + nominatim: + # https://github.com/geocoder-php/nominatim-provider + factory: Bazinga\GeocoderBundle\ProviderFactory\NominatimFactory + + # https://github.com/geocoder-php/BazingaGeocoderBundle/blob/master/doc/cache.md + cache: 'cache.geoloc.psr16' + cache_lifetime: 86400 + cache_precision: ~ diff --git a/config/packages/cache.yaml b/config/packages/cache.yaml new file mode 100644 index 0000000..21eca74 --- /dev/null +++ b/config/packages/cache.yaml @@ -0,0 +1,23 @@ +framework: + cache: + # Unique name of your app: used to compute stable namespaces for cache keys. + prefix_seed: platformecoop/ebs + + # The "app" cache stores to the filesystem by default. + # The data in this cache should persist between deploys. + # app: cache.adapter.filesystem + + # Other options include: + # Redis + app: cache.adapter.redis + default_redis_provider: "%env(REDIS_URL)%" + + # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) + #app: cache.adapter.apcu + + # Namespaced pools use the above "app" backend by default + pools: + # Specific pool for Geolocation stuff + cache.geoloc: + adapter: cache.app + default_lifetime: 3600 diff --git a/config/packages/debug.yaml b/config/packages/debug.yaml new file mode 100644 index 0000000..ad874af --- /dev/null +++ b/config/packages/debug.yaml @@ -0,0 +1,5 @@ +when@dev: + debug: + # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. + # See the "server:dump" command to start a new server. + dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml new file mode 100644 index 0000000..a503ee0 --- /dev/null +++ b/config/packages/doctrine.yaml @@ -0,0 +1,44 @@ +doctrine: + dbal: + url: '%env(resolve:DATABASE_URL)%' + + # IMPORTANT: You MUST configure your server version, + # either here or in the DATABASE_URL env var (see .env file) + server_version: '14' # to synchronize with docker-compose.yml + + orm: + auto_generate_proxy_classes: true + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + auto_mapping: true + mappings: + App: + is_bundle: false + dir: '%kernel.project_dir%/src/Entity' + prefix: 'App\Entity' + alias: App + +when@test: + doctrine: + dbal: + # "TEST_TOKEN" is typically set by ParaTest + dbname_suffix: '_test%env(default::TEST_TOKEN)%' + logging: false + +when@prod: + doctrine: + orm: + auto_generate_proxy_classes: false + query_cache_driver: + type: pool + pool: doctrine.system_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + + framework: + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system diff --git a/config/packages/doctrine_migrations.yaml b/config/packages/doctrine_migrations.yaml new file mode 100644 index 0000000..090fa16 --- /dev/null +++ b/config/packages/doctrine_migrations.yaml @@ -0,0 +1,7 @@ +doctrine_migrations: + migrations_paths: + # namespace is arbitrary but should be different from App\Migrations + # as migrations classes should NOT be autoloaded + 'DoctrineMigrations': '%kernel.project_dir%/migrations' + enable_profiler: false + organize_migrations: BY_YEAR diff --git a/config/packages/flysystem.yaml b/config/packages/flysystem.yaml new file mode 100644 index 0000000..0f69a58 --- /dev/null +++ b/config/packages/flysystem.yaml @@ -0,0 +1,100 @@ +parameters: + # physical storage path + upload_dir: '%kernel.project_dir%/public/storage/uploads' + category_upload_dir: '%upload_dir%/category' + user_upload_dir: '%upload_dir%/user' + product_upload_dir: '%upload_dir%/product' + + # relative URL path + base_path: '/storage/uploads' + category_base_path: '%base_path%/category' + user_base_path: '%base_path%/user' + product_base_path: '%base_path%/product' + + # S3 Bucket config + storage_bucket: '%env(resolve:STORAGE_BUCKET)%' + +# Read the documentation at https://github.com/thephpleague/flysystem-bundle/blob/master/docs/1-getting-started.md +flysystem: + storages: + category.storage: + adapter: 'local' + options: + directory: '%category_upload_dir%' + public_url: '%category_base_path%' + + # local + user.storage: + adapter: 'local' + options: + directory: '%user_upload_dir%' + public_url: '%user_base_path%' + + # Test S3 config locally with the min.io service (@see the docker compose files) +# user.storage: +# adapter: 'aws' +# visibility: public +# options: +# bucket: '%storage_bucket%' +# prefix: 'user' +# client: 'Aws\S3\S3Client' + + product.storage: + adapter: 'local' + options: + directory: '%product_upload_dir%' + public_url: '%product_base_path%' + default.storage: + adapter: 'local' + options: + directory: '%upload_dir%' + public_url: '%base_path%' + +# memory storage in the test env so we don't have to manipulate actual files +when@test: + flysystem: + storages: + category.storage: + adapter: 'memory' + public_url: '%category_base_path%' + user.storage: + adapter: 'memory' + public_url: '%user_base_path%' + product.storage: + adapter: 'memory' + public_url: '%product_base_path%' + default.storage: + adapter: 'memory' + public_url: '%base_path%' + +# S3 compatible bucket in the production environment +when@prod: + flysystem: + storages: + category.storage: + adapter: 'aws' + visibility: public + options: + bucket: '%storage_bucket%' + prefix: 'category' + client: 'Aws\S3\S3Client' + user.storage: + adapter: 'aws' + visibility: public + options: + bucket: '%storage_bucket%' + prefix: 'user' + client: 'Aws\S3\S3Client' + product.storage: + adapter: 'aws' + visibility: public + options: + bucket: '%storage_bucket%' + prefix: 'product' + client: 'Aws\S3\S3Client' + default.storage: + adapter: 'aws' + visibility: public + options: + bucket: '%storage_bucket%' + client: 'Aws\S3\S3Client' diff --git a/config/packages/fos_ckeditor.yaml b/config/packages/fos_ckeditor.yaml new file mode 100644 index 0000000..8a9bc50 --- /dev/null +++ b/config/packages/fos_ckeditor.yaml @@ -0,0 +1,10 @@ +# Read the documentation: https://symfony.com/doc/current/bundles/FOSCKEditorBundle/index.html + +twig: + form_themes: + - '@FOSCKEditor/Form/ckeditor_widget.html.twig' + +fos_ck_editor: + configs: + main_config: + toolbar: full diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml new file mode 100644 index 0000000..a5bf8ec --- /dev/null +++ b/config/packages/framework.yaml @@ -0,0 +1,29 @@ +# see https://symfony.com/doc/current/reference/configuration/framework.html +framework: + secret: '%env(APP_SECRET)%' + csrf_protection: true + http_method_override: false + + # Enables session support. Note that the session will ONLY be started if you read or write from it. + # Remove or comment this section to explicitly disable session support. + session: + # handler_id: null # native session handler + handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler + cookie_secure: auto + cookie_samesite: lax + storage_factory_id: session.storage.factory.native + + #esi: true + #fragments: true + php_errors: + log: true + + # i18n + # this parameter with allowed routing prefix, @see LocalesCompilerPass + enabled_locales: ['fr'] + +when@test: + framework: + test: true + session: + storage_factory_id: session.storage.factory.mock_file diff --git a/config/packages/hautelook_alice.yaml b/config/packages/hautelook_alice.yaml new file mode 100644 index 0000000..1940b65 --- /dev/null +++ b/config/packages/hautelook_alice.yaml @@ -0,0 +1,13 @@ +hautelook_alice: + fixtures_path: 'fixtures' # Path to which to look for fixtures relative to the project directory or the bundle path. May be a string or an array of strings. + root_dirs: + - '%kernel.project_dir%' + +when@dev: &dev + hautelook_alice: + fixtures_path: 'fixtures' + +when@test: *dev + +# An instance can be initialized thanks to the prod fixtures +when@prod: *dev diff --git a/config/packages/http_discovery.yaml b/config/packages/http_discovery.yaml new file mode 100644 index 0000000..2a789e7 --- /dev/null +++ b/config/packages/http_discovery.yaml @@ -0,0 +1,10 @@ +services: + Psr\Http\Message\RequestFactoryInterface: '@http_discovery.psr17_factory' + Psr\Http\Message\ResponseFactoryInterface: '@http_discovery.psr17_factory' + Psr\Http\Message\ServerRequestFactoryInterface: '@http_discovery.psr17_factory' + Psr\Http\Message\StreamFactoryInterface: '@http_discovery.psr17_factory' + Psr\Http\Message\UploadedFileFactoryInterface: '@http_discovery.psr17_factory' + Psr\Http\Message\UriFactoryInterface: '@http_discovery.psr17_factory' + + http_discovery.psr17_factory: + class: Http\Discovery\Psr17Factory diff --git a/config/packages/knp_paginator.yaml b/config/packages/knp_paginator.yaml new file mode 100644 index 0000000..43657ce --- /dev/null +++ b/config/packages/knp_paginator.yaml @@ -0,0 +1,4 @@ +# https://github.com/KnpLabs/KnpPaginatorBundle#configuration-example +knp_paginator: + template: + pagination: '@KnpPaginator/Pagination/bootstrap_v5_pagination.html.twig' diff --git a/config/packages/mailer.yaml b/config/packages/mailer.yaml new file mode 100644 index 0000000..198fc6f --- /dev/null +++ b/config/packages/mailer.yaml @@ -0,0 +1,10 @@ +framework: + mailer: + dsn: '%env(MAILER_DSN)%' + envelope: + sender: 'notifications@example.com' + +when@test: + framework: + mailer: + dsn: 'null://null' diff --git a/config/packages/meilisearch.yaml b/config/packages/meilisearch.yaml new file mode 100644 index 0000000..eaab125 --- /dev/null +++ b/config/packages/meilisearch.yaml @@ -0,0 +1,9 @@ +# Meilisearch +parameters: + meilisearchUrl: '%env(string:MEILISEARCH_URL)%' + meilisearchApiKey: '%env(string:MEILISEARCH_API_KEY)%' + +services: + _defaults: + autowire: true + autoconfigure: true diff --git a/config/packages/mercure.yaml b/config/packages/mercure.yaml new file mode 100644 index 0000000..f2a7395 --- /dev/null +++ b/config/packages/mercure.yaml @@ -0,0 +1,8 @@ +mercure: + hubs: + default: + url: '%env(MERCURE_URL)%' + public_url: '%env(MERCURE_PUBLIC_URL)%' + jwt: + secret: '%env(MERCURE_JWT_SECRET)%' + publish: '*' diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml new file mode 100644 index 0000000..ddc9c4c --- /dev/null +++ b/config/packages/messenger.yaml @@ -0,0 +1,33 @@ +framework: + messenger: + # https://symfony.com/doc/current/messenger/multiple_buses.html + default_bus: command.bus + buses: + query.bus: # $queryBus + command.bus: # $commandBus + + # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling. + failure_transport: failed + + transports: + # https://symfony.com/doc/current/messenger.html#transport-configuration + sync: 'sync://' + async: '%env(MESSENGER_TRANSPORT_DSN)%' + failed: '%env(MESSENGER_TRANSPORT_DSN)%?queue_name=failed' + + routing: + # Sync + '*': sync # default routing for all messages until not changed + + # Route your messages to the transports + # Async + # 'App\Message\YourMessage': async + +when@test: + framework: + messenger: + transports: + # replace with your transport name here (e.g., my_transport: 'in-memory://') + # For more Messenger testing tools, see https://github.com/zenstruck/messenger-test + async: 'test://' + # sync: 'test://' diff --git a/config/packages/misd_phone_number.yaml b/config/packages/misd_phone_number.yaml new file mode 100644 index 0000000..ae476b0 --- /dev/null +++ b/config/packages/misd_phone_number.yaml @@ -0,0 +1,6 @@ +# To persist libphonenumber\PhoneNumber objects, add the Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType mapping to your application's config. +# This requires: doctrine/doctrine-bundle +doctrine: + dbal: + types: + phone_number: Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType diff --git a/config/packages/mollie.yaml b/config/packages/mollie.yaml new file mode 100644 index 0000000..0157450 --- /dev/null +++ b/config/packages/mollie.yaml @@ -0,0 +1,11 @@ +services: + _defaults: + autowire: true # Automatically injects dependencies in your services. + autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + + # @see https://github.com/webbaard/payum-mollie#symfony-integration + app.payum.mollie.factory: + class: Payum\Core\Bridge\Symfony\Builder\GatewayFactoryBuilder + arguments: [PayHelper\Payum\Mollie\MollieGatewayFactory] + tags: + - { name: payum.gateway_factory_builder, factory: mollie } diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml new file mode 100644 index 0000000..8c9efa9 --- /dev/null +++ b/config/packages/monolog.yaml @@ -0,0 +1,61 @@ +monolog: + channels: + - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists + +when@dev: + monolog: + handlers: + main: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + channels: ["!event"] + # uncomment to get logging in your browser + # you may have to allow bigger header sizes in your Web server configuration + #firephp: + # type: firephp + # level: info + #chromephp: + # type: chromephp + # level: info + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine", "!console"] + +when@test: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + channels: ["!event"] + nested: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + +when@prod: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + buffer_size: 50 # How many messages should be saved? Prevent memory leaks + nested: + type: stream + path: php://stderr + level: debug + formatter: monolog.formatter.json + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine"] + deprecation: + type: stream + channels: [deprecation] + path: php://stderr diff --git a/config/packages/nelmio_alice.yaml b/config/packages/nelmio_alice.yaml new file mode 100644 index 0000000..0be7f2a --- /dev/null +++ b/config/packages/nelmio_alice.yaml @@ -0,0 +1,15 @@ +hautelook_alice: + root_dirs: + +when@dev: &dev + nelmio_alice: + functions_blacklist: + - 'current' + - 'shuffle' + - 'date' + - 'time' + - 'file' + - 'md5' + - 'sha1' + +when@test: *dev diff --git a/config/packages/nelmio_cors.yaml b/config/packages/nelmio_cors.yaml new file mode 100644 index 0000000..c766508 --- /dev/null +++ b/config/packages/nelmio_cors.yaml @@ -0,0 +1,10 @@ +nelmio_cors: + defaults: + origin_regex: true + allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] + allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] + allow_headers: ['Content-Type', 'Authorization'] + expose_headers: ['Link'] + max_age: 3600 + paths: + '^/': null diff --git a/config/packages/notifier.yaml b/config/packages/notifier.yaml new file mode 100644 index 0000000..ce92ec6 --- /dev/null +++ b/config/packages/notifier.yaml @@ -0,0 +1,29 @@ +framework: + notifier: + texter_transports: + sms_service: '%env(SMS_DSN)%' # change in your .env.local to test with a real API key + # sms_service: 'null://null' # use this to deactivate totally SMS + + channel_policy: + # use chat/slack, chat/telegram, sms/twilio or sms/nexmo + urgent: ['email'] + high: ['email'] + medium: ['email'] + low: ['email'] + + admin_recipients: + - { email: admin@example.com } + +when@dev: + framework: + notifier: + texter_transports: + fakesms: 'fakesms+email://mailer?to=TO&from=FROM' + +# don't send real SMS in test env, but we are still able to check that the code +# tried to send something with the AssertCount assertions (@see tests) +when@test: + framework: + notifier: + texter_transports: + sms_service: 'null://null' diff --git a/config/packages/nyholm_psr7.yaml b/config/packages/nyholm_psr7.yaml new file mode 100644 index 0000000..f135723 --- /dev/null +++ b/config/packages/nyholm_psr7.yaml @@ -0,0 +1,21 @@ +services: + # Register nyholm/psr7 services for autowiring with PSR-17 (HTTP factories) + Psr\Http\Message\RequestFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\ResponseFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\ServerRequestFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\StreamFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\UploadedFileFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\UriFactoryInterface: '@nyholm.psr7.psr17_factory' + + # Register nyholm/psr7 services for autowiring with HTTPlug factories + Http\Message\MessageFactory: '@nyholm.psr7.httplug_factory' + Http\Message\RequestFactory: '@nyholm.psr7.httplug_factory' + Http\Message\ResponseFactory: '@nyholm.psr7.httplug_factory' + Http\Message\StreamFactory: '@nyholm.psr7.httplug_factory' + Http\Message\UriFactory: '@nyholm.psr7.httplug_factory' + + nyholm.psr7.psr17_factory: + class: Nyholm\Psr7\Factory\Psr17Factory + + nyholm.psr7.httplug_factory: + class: Nyholm\Psr7\Factory\HttplugFactory diff --git a/config/packages/payum.yaml b/config/packages/payum.yaml new file mode 100644 index 0000000..91303df --- /dev/null +++ b/config/packages/payum.yaml @@ -0,0 +1,21 @@ +# https://github.com/Payum/PayumBundle +payum: + security: + token_storage: + App\Entity\PaymentToken: { doctrine: orm } + + storages: + App\Entity\Payment: { doctrine: orm } + + gateways: + # For tests but can also be used for offline payments like cash. + offline: + factory: offline + + # https://github.com/webbaard/payum-mollie + # fork for PHP 8.0+ + # see config/packages/mollie.yaml + mollie: + factory: mollie + apiKey: '%env(string:PAYUM_APIKEY)%' + method: creditcard # one of directdebit, creditcard or directdebit_oneoff diff --git a/config/packages/routing.yaml b/config/packages/routing.yaml new file mode 100644 index 0000000..4b766ce --- /dev/null +++ b/config/packages/routing.yaml @@ -0,0 +1,12 @@ +framework: + router: + utf8: true + + # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. + # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands + #default_uri: http://localhost + +when@prod: + framework: + router: + strict_requirements: null diff --git a/config/packages/security.yaml b/config/packages/security.yaml new file mode 100644 index 0000000..b75cfbc --- /dev/null +++ b/config/packages/security.yaml @@ -0,0 +1,67 @@ +security: + # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords + password_hashers: + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' + # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider + providers: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: email + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + main: + lazy: true + provider: app_user_provider + # checkers are in src/Security/Checker + user_checker: security.user_checker.chain.main + + # activate different ways to authenticate + # https://symfony.com/doc/current/security.html#the-firewall + + # https://symfony.com/doc/current/security/impersonating_user.html + switch_user: + parameter: _switch_user + + form_login: + login_path: app_login + check_path: app_login + enable_csrf: true + default_target_path: app_user_my_account + + # https://symfony.com/doc/current/security.html#logging-out + logout: + path: app_logout + target: app_login + + entry_point: App\Security\EntryPoint\AuthenticationEntryPoint + + login_throttling: + max_attempts: 2 + + # Easy way to control access for large sections of your site + # Note: Only the *first* access control that matches will be used + access_control: + - { path: ^/admin, roles: [ROLE_ADMIN, ROLE_GROUP_ADMIN] } + # to synchronize with MyAccountAction + - { path: ^/en/my-account/, roles: ROLE_USER } + - { path: ^/fr/mon-compte/, roles: ROLE_USER } + + role_hierarchy: + ROLE_ADMIN: [ROLE_USER, ROLE_ALLOWED_TO_SWITCH, ROLE_GROUP_ADMIN] + +when@test: + security: + password_hashers: + # By default, password hashers are resource intensive and take time. This is + # important to generate secure password hashes. In tests however, secure hashes + # are not important, waste resources and increase test times. The following + # reduces the work factor to the lowest possible values. + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: + algorithm: auto + cost: 4 # Lowest possible value for bcrypt + time_cost: 3 # Lowest possible value for argon + memory_cost: 10 # Lowest possible value for argon diff --git a/config/packages/sensio_framework_extra.yaml b/config/packages/sensio_framework_extra.yaml new file mode 100644 index 0000000..74ff738 --- /dev/null +++ b/config/packages/sensio_framework_extra.yaml @@ -0,0 +1,7 @@ +# https://stackoverflow.com/q/69809320/633864 +sensio_framework_extra: + router: + annotations: false + request: + converters: false + auto_convert: false diff --git a/config/packages/snc_redis.yaml b/config/packages/snc_redis.yaml new file mode 100644 index 0000000..39b94b5 --- /dev/null +++ b/config/packages/snc_redis.yaml @@ -0,0 +1,27 @@ +services: + _defaults: + autowire: true + autoconfigure: true + Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler: + arguments: + - '@snc_redis.session' + - { 'ttl': 3600 } + +# Define your clients here. The example below connects to database 0 of the default Redis server. +# +# See https://github.com/snc/SncRedisBundle/blob/master/docs/README.md for instructions on +# how to configure the bundle. +snc_redis: + clients: + default: + type: phpredis + alias: default + dsn: "%env(REDIS_URL)%" + logging: '%kernel.debug%' + + # use in framework. + session: + type: phpredis + alias: session + dsn: "%env(REDIS_URL)%/1" + logging: '%kernel.debug%' diff --git a/config/packages/stof_doctrine_extensions.yaml b/config/packages/stof_doctrine_extensions.yaml new file mode 100644 index 0000000..41edd78 --- /dev/null +++ b/config/packages/stof_doctrine_extensions.yaml @@ -0,0 +1,10 @@ +# Read the documentation: https://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html +# See the official DoctrineExtensions documentation for more details: https://github.com/doctrine-extensions/DoctrineExtensions/tree/main/doc +stof_doctrine_extensions: + default_locale: fr_FR + orm: + default: + timestampable: true + sluggable: true + sortable: true + tree: true diff --git a/config/packages/translation.yaml b/config/packages/translation.yaml new file mode 100644 index 0000000..0bf4061 --- /dev/null +++ b/config/packages/translation.yaml @@ -0,0 +1,16 @@ +parameters: + requirements_locales: null # @see LocalesCompilerPass + +framework: + default_locale: fr + translator: + default_path: '%kernel.project_dir%/translations' + fallbacks: + - fr +# providers: +# crowdin: +# dsn: '%env(CROWDIN_DSN)%' +# loco: +# dsn: '%env(LOCO_DSN)%' +# lokalise: +# dsn: '%env(LOKALISE_DSN)%' diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml new file mode 100644 index 0000000..c6943af --- /dev/null +++ b/config/packages/twig.yaml @@ -0,0 +1,16 @@ +twig: + default_path: '%kernel.project_dir%/templates' + form_themes: ['bootstrap_5_horizontal_layout.html.twig'] # bootstrap_5_layout.html.twig + globals: + app_version: '%app_version%' + upload_images_allowed_extensions: '%upload_maxsize_by_file%' + upload_maxsize_by_file: '%upload_maxsize_by_file%' + upload_maxsize_total: '%upload_maxsize_total%' + upload_max_images: '%upload_max_images%' + brand: '%brand%' + role_user: !php/const App\Entity\User::ROLE_USER + role_admin: !php/const App\Entity\User::ROLE_ADMIN + +when@test: + twig: + strict_variables: true diff --git a/config/packages/uid.yaml b/config/packages/uid.yaml new file mode 100644 index 0000000..c94558d --- /dev/null +++ b/config/packages/uid.yaml @@ -0,0 +1,4 @@ +framework: + uid: + # default_uuid_version: 7 + # time_based_uuid_version: 7 diff --git a/config/packages/validator.yaml b/config/packages/validator.yaml new file mode 100644 index 0000000..0201281 --- /dev/null +++ b/config/packages/validator.yaml @@ -0,0 +1,13 @@ +framework: + validation: + email_validation_mode: html5 + + # Enables validator auto-mapping support. + # For instance, basic validation constraints will be inferred from Doctrine's metadata. + #auto_mapping: + # App\Entity\: [] + +when@test: + framework: + validation: + not_compromised_password: false diff --git a/config/packages/web_profiler.yaml b/config/packages/web_profiler.yaml new file mode 100644 index 0000000..2805d5c --- /dev/null +++ b/config/packages/web_profiler.yaml @@ -0,0 +1,17 @@ +when@dev: + web_profiler: + toolbar: '%kernel.debug%' + intercept_redirects: false + + framework: + profiler: + only_exceptions: false + collect_serializer_data: true + +when@test: + web_profiler: + toolbar: false + intercept_redirects: false + + framework: + profiler: { collect: false } diff --git a/config/packages/webpack_encore.yaml b/config/packages/webpack_encore.yaml new file mode 100644 index 0000000..de39414 --- /dev/null +++ b/config/packages/webpack_encore.yaml @@ -0,0 +1,50 @@ +webpack_encore: + # The path where Encore is building the assets - i.e. Encore.setOutputPath() + output_path: '%kernel.project_dir%/public/build' + # If multiple builds are defined (as shown below), you can disable the default build: + # output_path: false + + # Set attributes that will be rendered on all script and link tags + script_attributes: + defer: true + # Uncomment (also under link_attributes) if using Turbo Drive + # https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change + # 'data-turbo-track': reload + # link_attributes: + # Uncomment if using Turbo Drive + # 'data-turbo-track': reload + + # If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials') + # crossorigin: 'anonymous' + + # Preload all rendered script and link tags automatically via the HTTP/2 Link header + # preload: true + + # Throw an exception if the entrypoints.json file is missing or an entry is missing from the data + strict_mode: false + + # If you have multiple builds: + # builds: + # pass "frontend" as the 3rg arg to the Twig functions + # {{ encore_entry_script_tags('entry1', null, 'frontend') }} + + # frontend: '%kernel.project_dir%/public/frontend/build' + + # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) + # Put in config/packages/prod/webpack_encore.yaml + # cache: true + +framework: + assets: + json_manifest_path: '%kernel.project_dir%/public/build/manifest.json' + +when@prod: + webpack_encore: + # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) + # Available in version 1.2 + cache: true + strict_mode: true + +when@test: + webpack_encore: + strict_mode: false diff --git a/config/packages/workflow.yaml b/config/packages/workflow.yaml new file mode 100644 index 0000000..a8d8d1f --- /dev/null +++ b/config/packages/workflow.yaml @@ -0,0 +1,56 @@ +# https://symfony.com/doc/current/workflow.html#configuration +# Get the generated services : +# $ php bin/console debug:autowiring workflow +# Generate the graphs in "/docs": +# $ make workflows +framework: + workflows: + service_request_status: + type: 'state_machine' + audit_trail: + enabled: true + marking_store: + type: 'method' + # for now workflow uses strings not enums + # @see https://github.com/symfony/symfony/issues/44211 + property: 'statusRaw' + supports: + - App\Entity\ServiceRequest + initial_marking: new + # @see ServiceRequestStatus + places: + - new + - to_confirm + - confirmed + - refused + - finished + # @see ServiceRequestStatusWorkflow + transitions: + # owner confirmation + accept: + from: new + to: to_confirm + # the owner modifies the dates and accept the request + modifyOwner: + from: new + to: to_confirm + # the recipient modifies the dates and ask the owner to validate again + modifyRecipient: + from: to_confirm + to: new + # recipient confirmation + confirm: + from: to_confirm + to: confirmed + # transaction manually finalized by the owner + finalize: + from: confirmed + to: finished + # transaction auto-finalized by the system + autoFinalize: + from: confirmed + to: finished + # refusal from the owner or recipient + refuse: + from: [new, to_confirm, confirmed] + to: refused diff --git a/config/packages_extra/doctrine.yaml b/config/packages_extra/doctrine.yaml new file mode 100644 index 0000000..035dd03 --- /dev/null +++ b/config/packages_extra/doctrine.yaml @@ -0,0 +1,11 @@ +# additional services related to Doctrine +services: + _defaults: + autowire: true + autoconfigure: true + + # Needed to activate autowiring for the Doctrine listener + App\Doctrine\Listener\: + resource: '../../src/Doctrine/Listener' + tags: + - { name: doctrine.orm.entity_listener } diff --git a/config/packages_extra/flysystem.yaml b/config/packages_extra/flysystem.yaml new file mode 100644 index 0000000..3ed296e --- /dev/null +++ b/config/packages_extra/flysystem.yaml @@ -0,0 +1,53 @@ +# additional services related to Flysystem +parameters: + # Allowed extensions for images + upload_images_allowed_extensions: ['png', 'jpg', 'jpeg'] + + # Allowed maxsize by file (mb) + upload_maxsize_by_file: 1 + + # Allowed maxsize for multiple uploads (mb) + upload_maxsize_total: 5 + + # Max number of photos/images by product + upload_max_images: 5 + + # S3 Bucket config + storage_endpoint: '%env(string:STORAGE_ENDPOINT)%' + storage_region: '%env(string:STORAGE_REGION)%' + storage_use_path_style_endpoint: '%env(bool:STORAGE_USE_PATH_STYLE_ENDPOINT)%' + storage_key: '%env(string:STORAGE_KEY)%' + storage_secret: '%env(string:STORAGE_SECRET)%' + +services: + _defaults: + autowire: true + autoconfigure: true + + _instanceof: + App\Twig\FlysystemImageInterface: + tags: [app.flysystem_image_extension] + + App\Twig\FlysystemImagesInterface: + tags: [app.flysystem_images_extension] + + App\Twig\: + resource: '../../src/Twig/' + + App\Twig\ImageExtensionCollection: + arguments: + - !tagged_iterator app.flysystem_image_extension + + App\Twig\ImagesExtensionCollection: + arguments: + - !tagged_iterator app.flysystem_images_extension + + Aws\S3\S3Client: + arguments: + - endpoint: '%storage_endpoint%' + version: 'latest' + region: '%storage_region%' + use_path_style_endpoint: '%storage_use_path_style_endpoint%' + credentials: + key: '%storage_key%' + secret: '%storage_secret%' diff --git a/config/packages_extra/mailer.yaml b/config/packages_extra/mailer.yaml new file mode 100644 index 0000000..5644767 --- /dev/null +++ b/config/packages_extra/mailer.yaml @@ -0,0 +1,17 @@ +# additional services related to the mailer +services: + _defaults: + autowire: true + autoconfigure: true + + _instanceof: + App\Mailer\Email\EmailInterface: + tags: ['app.email'] + + # we need to reload EmailInterface objects here so the correct tag is applied + App\Mailer\Email\: + resource: '../../src/Mailer/Email/' + + App\Mailer\EmailCollection: + arguments: + - !tagged_iterator app.email diff --git a/config/packages_extra/uuid.yaml b/config/packages_extra/uuid.yaml new file mode 100644 index 0000000..04d1e46 --- /dev/null +++ b/config/packages_extra/uuid.yaml @@ -0,0 +1,11 @@ +# additional services related to the uuid component +services: + _defaults: + autowire: true + autoconfigure: true + + # Activate these commands + Symfony\Component\Uid\Command\GenerateUlidCommand: ~ + Symfony\Component\Uid\Command\GenerateUuidCommand: ~ + Symfony\Component\Uid\Command\InspectUlidCommand: ~ + Symfony\Component\Uid\Command\InspectUuidCommand: ~ diff --git a/config/preload.php b/config/preload.php new file mode 100644 index 0000000..7cbe578 --- /dev/null +++ b/config/preload.php @@ -0,0 +1,7 @@ + symfony/mercure-bundle ### +###< symfony/mercure-bundle ### + +###> doctrine/doctrine-bundle ### + database: + ports: + - "5432" +###< doctrine/doctrine-bundle ### + +volumes: + storage: + minio_data: diff --git a/docker-compose.override.yml.dist b/docker-compose.override.yml.dist new file mode 100644 index 0000000..f3ac34c --- /dev/null +++ b/docker-compose.override.yml.dist @@ -0,0 +1,23 @@ +# start with make start-dev +services: + # http://localhost:61302/ + mailer: + image: schickling/mailcatcher + ports: + - "1081:1080" + - "1026:1025" + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:1080"] + interval: 10s + timeout: 5s + retries: 5 + + adminer: + image: adminer + ports: + - "8989:8080" + healthcheck: + test: ["CMD-SHELL", "curl --silent --fail http://127.0.0.1:8080/ || exit 1"] + interval: 10s + timeout: 30s + retries: 10 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..8c33061 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,13 @@ +version: "3.4" + +# Production environment override +services: + php: + environment: + APP_SECRET: ${APP_SECRET} + MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET} + + caddy: + environment: + MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET} + MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..55b97de --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,123 @@ +version: "3.4" + +services: + php: + build: + context: . + target: app_php + args: + SYMFONY_VERSION: ${SYMFONY_VERSION:-} + STABILITY: ${STABILITY:-stable} + restart: unless-stopped + volumes: + - php_socket:/var/run/php + healthcheck: + interval: 10s + timeout: 3s + retries: 3 + start_period: 30s + environment: + # Run "composer require symfony/orm-pack" to install and configure Doctrine ORM + DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@database:5432/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-14} + # Run "composer require symfony/mercure-bundle" to install and configure the Mercure integration + MERCURE_URL: ${CADDY_MERCURE_URL:-http://caddy/.well-known/mercure} + MERCURE_PUBLIC_URL: https://${SERVER_NAME:-localhost}/.well-known/mercure + MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!} + depends_on: + - redis + - database + + caddy: + build: + context: . + target: app_caddy + depends_on: + - php + environment: + SERVER_NAME: ${SERVER_NAME:-localhost, caddy:80} + MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!} + MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!} + restart: unless-stopped + volumes: + - php_socket:/var/run/php + - caddy_data:/data + - caddy_config:/config + ports: + # HTTP + - target: 80 + published: ${HTTP_PORT:-80} + protocol: tcp + # HTTPS + - target: 443 + published: ${HTTPS_PORT:-443} + protocol: tcp + # HTTP/3 + - target: 443 + published: ${HTTP3_PORT:-443} + protocol: udp + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider https://localhost/ || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + +# Mercure is installed as a Caddy module, prevent the Flex recipe from installing another service +###> symfony/mercure-bundle ### +###< symfony/mercure-bundle ### + +###> doctrine/doctrine-bundle ### + database: + image: postgres:${POSTGRES_VERSION:-14}-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-app} + # You should definitely change the password in production + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!} + POSTGRES_USER: ${POSTGRES_USER:-app} + volumes: + - db-data:/var/lib/postgresql/data:rw + # You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data! + # - ./docker/db/data:/var/lib/postgresql/data:rw + healthcheck: + test: ["CMD", "pg_isready", "-U", "app"] + interval: 10s + timeout: 5s + retries: 5 +###< doctrine/doctrine-bundle ### + +###> snc/redis-bundle ### +# Redis (official image) https://hub.docker.com/_/redis + redis: + image: redis:7-alpine + ports: + - '6389:6379' + healthcheck: + test: ["CMD-SHELL", "redis-cli -h 127.0.0.1 ping | grep 'PONG' || exit 1"] + interval: 10s + timeout: 30s + retries: 10 +###< snc/redis-bundle ### + +###< meilisearch/meilisearch-php ### + # https://docs.meilisearch.com/learn/cookbooks/docker.html#download-meilisearch-with-docker + meilisearch: + image: getmeili/meilisearch:v1.1 + volumes: + - ./data.ms:/data.ms + ports: + - "7700:7700" + environment: + - MEILI_MASTER_KEY=${MEILI_MASTER_KEY:-ms} + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:7700"] + interval: 10s + timeout: 5s + retries: 5 +###< meilisearch/meilisearch-php ### + +volumes: + php_socket: + caddy_data: + caddy_config: +###> doctrine/doctrine-bundle ### + db-data: +###< doctrine/doctrine-bundle ### diff --git a/docker/caddy/Caddyfile b/docker/caddy/Caddyfile new file mode 100644 index 0000000..6fa199b --- /dev/null +++ b/docker/caddy/Caddyfile @@ -0,0 +1,31 @@ +{ + # Debug + {$DEBUG} +} + +{$SERVER_NAME} + +log + +route { + root * /srv/app/public + mercure { + # Transport to use (default to Bolt) + transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db} + # Publisher JWT key + publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG} + # Subscriber JWT key + subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG} + # Allow anonymous subscribers (double-check that it's what you want) + anonymous + # Enable the subscription API (double-check that it's what you want) + subscriptions + # Extra directives + {$MERCURE_EXTRA_DIRECTIVES} + } + vulcain + push + php_fastcgi unix//var/run/php/php-fpm.sock + encode zstd gzip + file_server +} diff --git a/docker/php/conf.d/app.dev.ini b/docker/php/conf.d/app.dev.ini new file mode 100644 index 0000000..174eca2 --- /dev/null +++ b/docker/php/conf.d/app.dev.ini @@ -0,0 +1,6 @@ +; See https://docs.docker.com/desktop/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host +; See https://github.com/docker/for-linux/issues/264 +; The `client_host` below may optionally be replaced with `discover_client_host=yes` +; Add `start_with_request=yes` to start debug session on each request +xdebug.client_host = 'host.docker.internal' +memory_limit = -1 diff --git a/docker/php/conf.d/app.ini b/docker/php/conf.d/app.ini new file mode 100644 index 0000000..5f832c0 --- /dev/null +++ b/docker/php/conf.d/app.ini @@ -0,0 +1,15 @@ +expose_php = 0 +date.timezone = Europe/Paris +apc.enable_cli = 1 +session.use_strict_mode = 1 +zend.detect_unicode = 0 + +; https://symfony.com/doc/current/performance.html +realpath_cache_size = 4096K +realpath_cache_ttl = 600 +opcache.interned_strings_buffer = 16 +opcache.max_accelerated_files = 20000 +opcache.memory_consumption = 256 +opcache.enable_file_override = 1 + +upload_max_filesize = 6M diff --git a/docker/php/conf.d/app.prod.ini b/docker/php/conf.d/app.prod.ini new file mode 100644 index 0000000..993d481 --- /dev/null +++ b/docker/php/conf.d/app.prod.ini @@ -0,0 +1,2 @@ +opcache.preload_user = www-data +opcache.preload = /srv/app/config/preload.php diff --git a/docker/php/docker-entrypoint.sh b/docker/php/docker-entrypoint.sh new file mode 100755 index 0000000..0884add --- /dev/null +++ b/docker/php/docker-entrypoint.sh @@ -0,0 +1,66 @@ +#!/bin/sh +set -e + +# first arg is `-f` or `--some-option` +if [ "${1#-}" != "$1" ]; then + set -- php-fpm "$@" +fi + +if [ "$1" = 'php-fpm' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then + # Install the project the first time PHP is started + # After the installation, the following block can be deleted + if [ ! -f composer.json ]; then + CREATION=1 + composer create-project "symfony/skeleton $SYMFONY_VERSION" tmp --stability="$STABILITY" --prefer-dist --no-progress --no-interaction --no-install + + cd tmp + composer require "php:>=$PHP_VERSION" + composer config --json extra.symfony.docker 'true' + cp -Rp . .. + cd - + + rm -Rf tmp/ + fi + + if [ "$APP_ENV" != 'prod' ]; then + composer install --prefer-dist --no-progress --no-interaction + fi + + if grep -q ^DATABASE_URL= .env; then + # After the installation, the following block can be deleted + if [ "$CREATION" = "1" ]; then + echo "To finish the installation please press Ctrl+C to stop Docker Compose and run: docker compose up --build" + sleep infinity + fi + + echo "Waiting for db to be ready..." + ATTEMPTS_LEFT_TO_REACH_DATABASE=60 + until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(bin/console dbal:run-sql "SELECT 1" 2>&1); do + if [ $? -eq 255 ]; then + # If the Doctrine command exits with 255, an unrecoverable error occurred + ATTEMPTS_LEFT_TO_REACH_DATABASE=0 + break + fi + sleep 1 + ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1)) + echo "Still waiting for db to be ready... Or maybe the db is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left" + done + + if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then + echo "The database is not up or not reachable:" + echo "$DATABASE_ERROR" + exit 1 + else + echo "The db is now ready and reachable" + fi + + if [ "$( find ./migrations -iname '*.php' -print -quit )" ]; then + bin/console doctrine:migrations:migrate --no-interaction + fi + fi + + setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var + setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var +fi + +exec docker-php-entrypoint "$@" diff --git a/docker/php/docker-healthcheck.sh b/docker/php/docker-healthcheck.sh new file mode 100644 index 0000000..f322de5 --- /dev/null +++ b/docker/php/docker-healthcheck.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +if env -i REQUEST_METHOD=GET SCRIPT_NAME=/ping SCRIPT_FILENAME=/ping cgi-fcgi -bind -connect /var/run/php/php-fpm.sock; then + exit 0 +fi + +exit 1 diff --git a/docker/php/php-fpm.d/zz-docker.conf b/docker/php/php-fpm.d/zz-docker.conf new file mode 100644 index 0000000..9f454cd --- /dev/null +++ b/docker/php/php-fpm.d/zz-docker.conf @@ -0,0 +1,8 @@ +[global] +daemonize = no +process_control_timeout = 20 + +[www] +listen = /var/run/php/php-fpm.sock +listen.mode = 0666 +ping.path = /ping diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..63f0942 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,77 @@ +# Plateforme EBS + +« Simplicity is the ultimate sophistication » - Leonardo da Vinci + +## Prerequisites (at least) + +* docker v20.10+ (`docker --version`) + +## Initializing and starting the project with Docker + +Clone the project: + + git clone git@github.com:ApesHDF/EBS.git + +Check that your 80 and 443 ports are free, then build and start the Docker containers: + + make build + make start + +Access `https://localhost` in your browser and accept the security risk. + +You should have access now to: + +* Main project : https://localhost +* Meilisearch : http://localhost:7700/ + +To access the dev tools, run= + + make start-dev + +You should have access now to: + +* Adminer : http://localhost:8989/?pgsql=database&username=app&db=app&ns=public&select=group +* Maildev : http://localhost:1080 + +## Makefile + +Check the [Makefile](Makefile) file to see all available commands. +In this project, the commands must be called outside the container. +If you want to use the Makefile from within the PHP container, just add `-n` +to the make call, eg: + + make stan -n + +Which outputs: + + docker compose exec php ./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit 1G + +Enter the PHP container: + + make sh + +Then run the wanted command without the docker part (`docker compose exec`): + + php ./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit 1G + +It's generally more conveniant to run Symfony commands inside the container: + + bin/console debug:container + +## Development + +Create the dev database and load fixtures: + + make load-fixtures + +Create the test database and load fixtures: + + make load-test-fixtures + +Run the tests and generate the code coverage report: + + make coverage + +Run all checks like the Github CI: + + make ci diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..8d04453 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,8 @@ +# Architecture + +Here is the diagram of the global architecture, presenting the high level components +and their relations. + +Check out the other documentation files for more details about a specific component. + +![MCD](images/archi.png "Achitecture") diff --git a/docs/composer.md b/docs/composer.md new file mode 100644 index 0000000..41d0127 --- /dev/null +++ b/docs/composer.md @@ -0,0 +1,35 @@ +# Composer + +This file documents the content of the `composer.json` for custom stuff. + + +## webbaard/payum-mollie + +For the Mollie gatework we use a fork of: + + https://github.com/webbaard/payum-mollie + +Which is itself a fork of: + + https://github.com/PayHelper/payum-mollie + +This last payum Mollie gateway is not up to date. +That's why we use the fork that supports the last Mollie 2.x API and SDK. + +We did a copy of the fork to avoid being tied to a specific vendor. +The fork is in the `coopTilleuls` vendor, this organization is behind the +[API Platform](https://github.com/api-platform/) open source project and can be +trusted. + +As the library is about payments, it is important to mitigate [supply chain attacks](https://dunglas.dev/2023/05/mitigate-attacks-on-your-php-supply-chain/). + +As soon as the the offical PayHelper Mollie gate will be updated, we will switch +to the official gateway and archive this temporary fork. +A [PR](https://github.com/PayHelper/payum-mollie/pull/8) has been open. + + +## easycorp/easyadmin-bundle + +The version is fixed to `v4.5.1`, to avoid a problem with enumerations. +The last time I tried, the issue was still there. +Check if it can be fixed more easily with the `4.6` version. diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..6459fcb --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,137 @@ +# Configuration + +This page documents the settings that can be configured as environment varibles +for the production environment. +Defaults value are shown in the [.env](../.env) file. + + +## Application + +| name | default value | +|--------------|---------------| +| APP_ENV | prod | +| APP_DEBUG | false | +| APP_SECRET | - | + +SMS_DSN=null://nullThe env and debug should always keep these values in the prod environement. +The secret is a random string that can be changed at each deploy. + + +## Database + + DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@database:5432/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-14} + +There are 4 parameters that should be configured as environment variables : + +| name | default value | +|-------------------|----------------| +| POSTGRES_USER | app | +| POSTGRES_PASSWORD | !ChangeMe! | +| POSTGRES_DB | app | +| POSTGRES_VERSION | 14 | + +The `POSTGRES_VERSION` should not be changed. + +The default DSN to access the database when using the Docker setup is `database`. + + +## Emails + + MAILER_DSN= + +Check out the [Symfony documentation](https://symfony.com/doc/current/mailer.html#using-built-in-transports). +There are a number of services that can be used. +For example, to use a standart SMTP server use: + + MAILER_DSN=smtp://mailer:1025 + +Where mailer is the DSN of your SMTP server and 1025 the port to use. + +Yo use Gmail with a secret key use: + + MAILER_DSN=gmail://email@example.com:secretkey@default + +Note that to avoid having SPAM issues, you should use a dedicated service like +Twilio, Sendgrid, Mailingblue... + +## SMS + + SMS_DSN=null://null + +This is the main parameter to send DNS. If you leave `null://null`, nothing will +be send without errors. It can be useful when having issues with your SMS provider +and wanting to disable it temporarly. + +For example, to use a service like Twilio, the parameters should look like: + + SMS_DSN=twilio://AccountSID:AuthToken@default?from=%2BFROMNUMBER + + +## Meilisearch + + MEILISEARCH_URL=http://meilisearch:7700 + MEILISEARCH_API_KEY=ms + +A local meilisearch instance is used by default. +But, you can also use a [managed service](https://cloud.meilisearch.com), in this +case, the parameters should look like: + + MEILISEARCH_URL=https://ms-id-id.subdomain.meilisearch.io + MEILISEARCH_API_KEY=f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6 + + +## Payum/Mollie + + PAYUM_APIKEY=test_FRabcdefghijklmnopqrstuvwxyzab + PAYUM_GATEWAY=mollie + +Payum with the Mollie gateway is used. +For now the mollie Gateway must be used, but, as Payum is used, another gateway +like Stripe could be added with some modifications on the code (feel to create a +PR for this). The are [official Stripe Gateways for Mollie](https://github.com/Payum/Payum/blob/master/docs/supported-gateways.md#official). + +To configure your Mollie account, access the [dashboard](https://my.mollie.com/dashboard/login?lang=en). +Create an account. + +Click on the ["Developer" page](https://my.mollie.com/dashboard/org_17065949/developers/api-keys) +and create a test key. +The test key has the format `test_xxxxxxxxxxxxx`, then put this value in the `PAYUM_APIKEY` +variable. +The test key can be used immediatly, but to have a production key you will have +to send documents to confirm the identity of your company and to be able to start +to receive payments on your bank account. + +Note, that this step can take some time, and should start the procedure the sooner +possible. +Meanwhile, you will have access the "fake" payment page to similate payments without +having to enter an actual credit card. + +Once your account is validated, replace the test API key by the live API key. + + +## Files/S3 bucket + +The goal here is to have persistent data in the production environment. + +Locally, the local file storage is used. +For tests, a memory storage is used. +In the production environment a S3 compatible bucket is used. +It has been tested with a [min.io](https://min.io/) service, there is a docker setup +available in the docker compose files. +Here are the default settings when using the min.io container: + + STORAGE_BUCKET=images + STORAGE_ENDPOINT=http://storage:9000 + STORAGE_REGION=us-east-1 + STORAGE_USE_PATH_STYLE_ENDPOINT=true + STORAGE_KEY=app + STORAGE_SECRET=!ChangeMe! + +If you want to use a managed service, change the values of these parameters to +match those of your provider. +Note that when using the min.io with the dev environment is just to test the S3 +configuration. +There is config example in the [config/packages/flysystem.yaml](../config/packages/flysystem.yaml) + +The bucket must be configured to be public and the default visibulity of each storage +entry should also be public. diff --git a/docs/crons.md b/docs/crons.md new file mode 100644 index 0000000..1b7a933 --- /dev/null +++ b/docs/crons.md @@ -0,0 +1,80 @@ +# CRON + +The following CRON must be installed: + +## Handle membership end: + +* remove the group association from the user +* remove the products from the group and change their visibility +* notify the user by email and SMS + + + bin/console app:end-membership --env=prod + + +## Notifiy users for membership expiration + +* the day before the membership is over: + + + bin/console app:notify-membership-expiration 1 --env=prod + +* A week before the membership is over: + + + bin/console app:notify-membership-expiration 7 --env=prod + +As you can see, can can run and pass the number of days you want. +If you want to notify one month before the end of annual membershipt, the query +should be modified to exclude monthly membership. + + +## Notify users for services requests start and end + +Notify owner and recipient when a service requests starts tomorrow: + + bin/console app:notify-service-request-dates start --env=prod + +Notify owner and recipient when a service request end tomorrow: + + bin/console app:notify-service-request-dates end --env=prod + + +## Help + +All commands are documented. You can get it by using the help command, eg: + + bin/console help app:notify-membership-expiration + +```shell +Description: +Notify expiring membership. + +Usage: +app:notify-membership-expiration + +Arguments: +days Number of days from tomorrow (1 = notifiy members expiring tomorrow) + +Options: +-h, --help Display help for the given command. When no command is given display help for the list command +-q, --quiet Do not output any message +-V, --version Display this application version +--ansi|--no-ansi Force (or disable --no-ansi) ANSI output +-n, --no-interaction Do not ask any interactive question +-e, --env=ENV The Environment name. [default: "dev"] +--no-debug Switch off debug mode. +-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + +Help: +Notify expiring membership. + +COMMAND: +App\Command\NotifyMembershipExpirationCommand + +DEV: +bin/console app:notify-membership-expiration -vv + +PROD: +bin/console app:notify-membership-expiration --env=prod --no-debug +``` diff --git a/docs/database.md b/docs/database.md new file mode 100644 index 0000000..84cc662 --- /dev/null +++ b/docs/database.md @@ -0,0 +1,129 @@ +# Database + + +## Introduction + +The database choosen is a Postgres 14 database as it integrates well with Symfony +and Doctrine. +When developping you can use the `make load-fixtures` command to reload a database +from scratch with some data samples. + + +## Docker + +In the docker hub, the database is available with the `database` DSN. +Check out the default settings in the [docker-compose.yml](../docker-compose.yml) +file. + + +## Schema + +Here is the current schema of the dabase: + +![MCD](images/mcd.png "MCD") + +It has 19 tables divided in several domains: + +* users +* groups +* products +* service requests +* payments +* menu +* pages +* configuration + +### Service requests + +In this application, the technical name for loans and services (services are not related to objects) +are service requests. + +Services requests are linked to products. + +A product can be an object or a service. + +The service requests follow a strict workflow in order to garantee the date integrity. + +Here is the workflow in place: + +![MCD](images/service_request_status_workflow.png "The service request workflow") + + +### System tables + +* The migrations are stored in the `doctrine_migration_table`. +* The messages are store in `messenger_messages` + +Note that for now all messages are synchronous, but if you change to change +this for performance reasons, the messages will be stored in these tables ans then +processed by the messenger consumer. + +Check out the [Symfony documention](https://symfony.com/doc/current/messenger.html#consuming-messages-running-the-worker) +on this subject. + +It's up to your responsability to setup the CRON that will consume the asynchronous +messages. + + +## Doctrine + +The description of each property/field is described in each entity: + +Example: + +```php +/** + * A user must be enabled to be able to login. If a user tries to login with + * a deactivated account, then he will have a specific message indicating + * the reason he can't login. He should be adviced to contact the instance administrator. + */ +#[ORM\Column(type: 'boolean', nullable: false)] +protected bool $enabled = true; +``` + +So, open the entity Doctrine files in the [src/Entity](../src/Entity) directory +to have the description of each property. + + +## Development + +To access the database with a GUI, after running `make start` you can run +`make start-dev` and enter http://localhost:8989/?pgsql=database&username=app&db=app&ns=public&dump=service_request +to access the adminer interface which is a small convenient tool to view the +tables and data they contain. + + +## Tests + +Fixtures are loaded depending on the current environment. +In dev and test the same fixtures are loaded (`dev` is a symbolik link to `test`, +see [fixtures/](../fixtures). + + +## Production + +A database with the minimal data to bootstrap the application is available. +It can be loaded with: + + make load-prod-fixtures + +It should only be used one time, or if you want to reset the database for testing +purpose. +Use it with care as it will delete all existing data of the current configured database. + + +## Migrations + +When the project will evolve, Doctrine migrations will be provided. +Retrieve the migrations in your project, then run: + + bin/console doctrine:migrations:migrate --env=prod + +Use the `--no-interaction` option to skip the interactive mode. +Use this option with care. + +Migrations will be documented in each file in the main docblock, so open them to +precisely understand what will be modified. + +Also understand, that depending on the cases, sometimes a migration cannot be undone. +It's your responsability to make a full backup of your data before running migrations. diff --git a/docs/front-api-calls.md b/docs/front-api-calls.md new file mode 100644 index 0000000..787fea3 --- /dev/null +++ b/docs/front-api-calls.md @@ -0,0 +1,19 @@ +# API calls from the Front + +To make an API call from the Front you've got to use native [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) +function in a [Stimulus](https://stimulus.hotwired.dev/) controller. + +Do not use hard-coded path in your Controller. Get the path from the back in your +twig template and forward it to your Stimulus Controller with a [Value attribute](https://stimulus.hotwired.dev/reference/values). + +Example for the product component: + +```html + +data-product-route-value="{{ path('_api_/product/{id}/switchStatus_post', {id: product.id}) }}" +``` + +```javascript +// assets/controllers/product_controller.js +const response = await fetch(this.routeValue, { method: 'POST' }) +``` diff --git a/docs/geoloc.md b/docs/geoloc.md new file mode 100644 index 0000000..4b0fa27 --- /dev/null +++ b/docs/geoloc.md @@ -0,0 +1,38 @@ +# Geoloc / GIS + +* https://symfony-devs.slack.com/archives/C3F1WBW5R/p1665562230485859 + +## Symfony bundle + +* https://github.com/geocoder-php/BazingaGeocoderBundle/blob/master/doc/index.md + +This bundle handles multiples providers. +The nominatim provider is free and can be chained with paying provider in case it +doesn't work or the free service isn't stable enough for the project needs. + +## OpenStreetmap (Nominatim) Provider + +* https://wiki.openstreetmap.org/wiki/Nominatim +* https://github.com/geocoder-php/nominatim-provider + +This provider is free and is a good starting point. + +Be careful that it has restrictions and the consummer must respect +the [policy](https://operations.osmfoundation.org/policies/nominatim/). + + +## Cache + +A cache is implemented, so when a user does the same query, the result cache is +used instead of querying the geocoding service another time for the same query. + + +## CLI Tests + +Getting the information for a given address: + + bin/console geocoder:geocode "82 Rue Winston Churchill, 59160, Lomme, FRANCE" + +Clearing the specific geoloc cache: + + bin/console cache:pool:clear cache.geoloc diff --git a/docs/images/apache2-default-page.png b/docs/images/apache2-default-page.png new file mode 100644 index 0000000..f275c8d Binary files /dev/null and b/docs/images/apache2-default-page.png differ diff --git a/docs/images/archi.png b/docs/images/archi.png new file mode 100644 index 0000000..c761ada Binary files /dev/null and b/docs/images/archi.png differ diff --git a/docs/images/base-color.png b/docs/images/base-color.png new file mode 100644 index 0000000..477e30c Binary files /dev/null and b/docs/images/base-color.png differ diff --git a/docs/images/mcd.png b/docs/images/mcd.png new file mode 100644 index 0000000..c62cc42 Binary files /dev/null and b/docs/images/mcd.png differ diff --git a/docs/images/service_request_status_workflow.png b/docs/images/service_request_status_workflow.png new file mode 100755 index 0000000..a2d3619 Binary files /dev/null and b/docs/images/service_request_status_workflow.png differ diff --git a/docs/images/updated-color.png b/docs/images/updated-color.png new file mode 100644 index 0000000..cc3d055 Binary files /dev/null and b/docs/images/updated-color.png differ diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..a278b61 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,248 @@ +# Installation + +This is an example of installation of the project on a virtual private server. +I has been installed on a [OVH VPS](https://www.ovhcloud.com/fr/vps/), the smallest +one, its specifications are: + +* 1 vCore +* 2 Go RAM +* 20 Go SSD SATA +* 100 Mbit/s unlimited +* Ubuntu 22.04 LTS + +Of course any hoster and servers type can be used. +Small modifications may be required depending on the used operating system. +But as the installation process is quite generic, ony small changes should be required +to fit your operating system needs. + +Some links are provided for reference. +Of course you are free to install the way you want the various required component +it's just a basic working example that can be adapted. + +I havent's put sudo command, it depends on wheter you run an administrator account +or not. +If not, you will have to prefix some commands with sudo. + + +## Server update ([source](https://www.linuxstart.com/install-php-ubuntu/)): + +First we must update the base packages of the server: + + apt update && sudo apt upgrade -y + apt-get install linux-headers-generic make + snap install nvim --classic + ([ -f /var/run/reboot-required ] && sudo reboot -f) + + +## PHP Installation + + apt install php php-cli php-common php-mbstring php-xmlrpc php-soap php-gd php-xml php-intl php-mysql php-cli php-zip php-curl php-apcu php-redis -y + +Verify the cli: + + php --version + +We should have the following output: + + PHP 8.1.2-1ubuntu2.11 (cli) (built: Feb 22 2023 22:56:18) (NTS) + Copyright (c) The PHP Group + Zend Engine v4.1.2, Copyright (c) Zend Technologies + with Zend OPcache v8.1.2-1ubuntu2.11, Copyright (c), by Zend Technologies + +Here the defautl PHP version is used but it is better to use the `ondrej/php` PPA +repository to have the last minor PHP version. + + +## Apache installation ([source](https://petri.com/install-apache-ubuntu-linux/)) + + apt install apache2 + systemctl start apache2 + systemctl enable apache2 + service apache2 status + +It should display (green ●): + + ● apache2.service - The Apache HTTP Server + +And we should see the default Apache page when accessing the server with HTTP: + +![Apache2](images/apache2-default-page.png "Apache2") + +(eg: http://vps-9cd99999.vps.ovh.net) + + + +## Generate a SSH key ([source](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent)) + +This section is only needed if you want to fetch the project from a private repository. +You may also not need this depending of your deployment process. + + ssh-keygen -t ed25519 -C "ubuntu@vps-6cd61302" + eval `ssh-agent -s` + ssh-add + + +## Cloning the project + +Git is already installed on this distribution. +You must install it if it isn't available. + + cd /var/www + git clone git@github.com:ApesHDF/EBS.git + + +## Composer ([source](https://getcomposer.org/download/)) + +Follow the instructions on https://getcomposer.org/download/. + +Then should have composer available + + composer --version + +Which gives: + + Composer version 2.5.5 2023-03-21 11:50:05 + +Install the project dependencies: + + cd /var/www/plateformcoop-ebs + composer install --ignore-platform-req=php + +The `--ignore-platform-` options is required if you have a PHP version below `8.1.11`. + + +## Redis installation + + apt-get install redis + + +## Meilisearch [cloud service](https://cloud.meilisearch.com) + +You can install a Meilsearch local instance but you can also use the cloud free +service. +It allows to have 100k documents and 10k search by month. +It works well for testing purpose. +Then can you always install a local Meilisearch instance when you reached the free +tier threshold. + +Once you cloud instance is created, get the instance URL and the password. +You have to use them for the `MEILISEARCH_URL` and `MEILISEARCH_API_KEY` variables +(check below). + + +## Postgres 14 installation ([source](https://computingforgeeks.com/install-postgresql-14-on-ubuntu-jammy-jellyfish/)) + + apt install vim curl wget gpg gnupg2 software-properties-common apt-transport-https lsb-release ca-certificates + apt install postgresql-14 postgresql-client + systemctl status postgresql@14-main.service + sudo -u postgres psql -c "SELECT version();" + +### Create the database + + sudo -u postgres psql + create database app; + create user app with encrypted password '!ChangeMe!'; + grant all privileges on database app to app; + +### Create the tables and load the initial fixtures + + bin/console doctrine:schema:create --env=prod + bin/console doctrine:migrations:migrate --env=prod --no-interaction + bin/console messenger:setup-transports --env=prod + bin/console hautelook:fixtures:load --no-interaction -vv --no-bundles --env=prod + +## Apache configuration + +Here is a simple vhost: + + nvim /etc/apache2/sites-available/000-default.conf + +And put: + + + ServerAdmin admin@example.com + DocumentRoot /var/www/plateformcoop-ebs/public + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + + AllowOverride All + Require all granted + FallbackResource /index.php + + + +Of course you should install a SSL certificate later with [letsencrypt](https://letsencrypt.org/) +for example. + + +## Node and Yarn installation + +Here are some resources: + +* https://www.howtoforge.com/how-to-install-yarn-on-ubuntu-22-04/ +* https://linuxize.com/post/how-to-install-node-js-on-ubuntu-22-04/ +* https://www.digitalocean.com/community/tutorials/how-to-install-and-use-the-yarn-package-manager-for-node-js + +We must have at least Node 14 and Yarn 1.22 + + wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash + sudo apt install nvm + +Close and reopen the session so nvm is available: + + nvm install 14 + apt install npm + npm install -g yarn + +Then run: + + cd /var/www/plateformcoop-ebs + yarn install + yarn build + + +## Files + +Be careful that is you run the commands as root some files may not have the correct +rights. +The following directory should be owned by the web server user: + + chown -R /var/www/plateformcoop-ebs/public + chown -R /var/www/plateformcoop-ebs/var + + +## Parameters + +You can create a `.env.local` file containing: + + APP_ENV=prod + APP_DEBUG=0 + APP_SECRET=xxxxxxx + MEILISEARCH_URL=https://ms-xxxxxxxx-3263.lon.meilisearch.io + MEILISEARCH_API_KEY=xxxxxxx + PAYUM_APIKEY=test_FRxxxxxx + REDIS_URL=redis://localhost:6379 + DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=14&charset=utf8" + MAILER_DSN=smtp://mailer:1025 + SMS_DSN=null://null + +Change the Meilisearch parameters you received before. +With this setup, mails are sent with a local SMTP server and SMS are disabled. + +Then you can run the Meilisearch index command: + + sudo bin/console app:index-products --env=prod + +That's it, access the root URL of your server, you should see the home page. +If you have a 500 error, you can change `APP_DEBUG` to 1 to see the errors. + +More information about the parameters in this [file](./configuration.md). + + +## Conclusion + +This is a working basic installation process. +Of course, it it better to use a deployment tool like [EasyDeploy](https://github.com/EasyCorp/easy-deploy-bundle), +[Capistrano](https://github.com/capistrano/capistrano) or other ones. diff --git a/docs/symfony.md b/docs/symfony.md new file mode 100644 index 0000000..5e4e5ff --- /dev/null +++ b/docs/symfony.md @@ -0,0 +1,88 @@ +# Symfony + +We tried to respect Symfony best practices. +Everything is not perfect of course, there is still some cleanup to do but globally +the project is clean. + +* PHPStan is at the maximum level without ignore annotations +* Code coverage is at 100% (some mock are used) +* php-cs-fixer is used + +DTO are used almost everywhere and we tried to avoid using arrays. +This makes the code more robust and easer to read. It also enable more autocompletion +with the code editors. + + +## Global architecture + +A simplified version of CQRS is used. +It it required when doing more complex processing +to have a better decoupling between controllers and services. +For more simple actions, like editing the user profile, standard forms are used. +CQRS stuff can be found in: + +* Message +* MessageBus +* MessageHandler + +For now all messages are synchronous but everything is ready to pass some messages +to asynchronous if there are too heavy processes. + + +## Doctrine + +We used the make bundle to generate entities. +Fixtures are loaded thanks to the Alice bundle. +These fixtures are used fo tests. +We tried to use Doctrine in the most standard way. +There are some behaviors used (timestamble, nested set) and UUID are used for all +table main identifiers. +Doctrine migrations are initialized. + + +## EasyAdmin + +We tried to get the maximum out of EasyAdmin without adding to much custom code. + +* There is custom code to be able to use Flysystem +* There is JavaScript code for files uploads + +The most important thing is the part to securize the access for group administrators +because they only need to access their data. + + +## Api Platform + +API Platform is used for some methods in the user connected space. +It's more to showcase the use of API Platform 3 along with Symfony 6, UX and Stimulus. + + +## Tests + +PHPUnit is used and the code coverage is 100%. These tests are grouped by type: + +* API tests +* End to end tests +* Functional tests +* Integration tests +* Unit tests + +The structure in each of this directory reflects what we can find in the `src/` folder. + +The coverage can by generated with: + + make coverage + +Some tests need to be cleaned the whole test suite is stable. +Mocks are used for the Geocoder calls because 500 errors where raised the GitHub CI. +So it is to avoid this. +Translations are deactivated in the test env so we can use key code instead of real +texts, this makes the tests less fragile. + + +## Workflow + +The workflow component is used the manage the state of the loans which is at the +heart of the application. +The loan state workflow can be found in [database.md](database.md). +The workflow Twig helpers are used in the templates to keep the templates clean. diff --git a/docs/theme.md b/docs/theme.md new file mode 100644 index 0000000..6f589b8 --- /dev/null +++ b/docs/theme.md @@ -0,0 +1,48 @@ +# Theme + +This platform use [Bootstrap](https://getbootstrap.com/). For customize boostrap, +override `./assets/styles_custom-variables.css`. + +## Example + +The base value for the color $primary is: + +``` +$blue-500: #0D6EFD; +$dark: $blue-500; +``` + +You can see this in the navbar. For the default theme, we have choice the background-color dark with the bootstrap class: `navbar-dark bg-dark` + +``` + +``` + +![Basic $primary color](images/base-color.png) + + +### Customize! + +For add your color theme, go to `/assets/styles_custom-variables.css`. + +First, declaring your color with a name and value: `$salmon: #FFA07A;` + +Next, let's assign this value to the variable `$primary` of boostrap : `$primary: $salmon;` + +[The other bootstrap variable name](https://getbootstrap.com/docs/5.0/customize/color/) + +Then, change the default navbar class with our new color. Attention, for the example we have choice to assign our color to the `$primary` bootstrap color. For see the new color, use `navbar-primary bg-primary` + +``` + +``` + +TADA ! We have customized the color !! + +![Updated color](images/updated-color.png) + +For more information, see the [Bootstrap customize doc](https://getbootstrap.com/docs/5.3/customize/overview/). diff --git a/docs/uploads.md b/docs/uploads.md new file mode 100644 index 0000000..e7bf586 --- /dev/null +++ b/docs/uploads.md @@ -0,0 +1,8 @@ +# Uploads settings + +To change the file upload max size, see: + +- in `docker/php/conf.d/app.ini` + - `upload_max_size` + - `post_max_size` (if you need more than the 8M default) +- `nginx.ingress.kubernetes.io/proxy-body-size` annotation in `helm/chart/values.yaml` diff --git a/fixtures/.gitignore b/fixtures/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/fixtures/dev b/fixtures/dev new file mode 120000 index 0000000..b59f7e3 --- /dev/null +++ b/fixtures/dev @@ -0,0 +1 @@ +test/ \ No newline at end of file diff --git a/fixtures/prod b/fixtures/prod new file mode 120000 index 0000000..b59f7e3 --- /dev/null +++ b/fixtures/prod @@ -0,0 +1 @@ +test/ \ No newline at end of file diff --git a/fixtures/prod-boot/category.yaml b/fixtures/prod-boot/category.yaml new file mode 100644 index 0000000..bd5c149 --- /dev/null +++ b/fixtures/prod-boot/category.yaml @@ -0,0 +1,18 @@ +App\Entity\Category: + # —— Templates ————————————————————————————————————————————————————————————— + category_template (template): + enabled: true + + category_object (template, extends category_template): + type: !php/enum App\Enum\Product\ProductType::OBJECT + + category_service (template, extends category_template): + type: !php/enum App\Enum\Product\ProductType::SERVICE + + # —— Objects ——————————————————————————————————————————————————————————————— + category_object_1 (extends category_object): + name: Catégorie objets 1 + + # —— Services —————————————————————————————————————————————————————————————— + category_service_1 (extends category_service): + name: Catégorie services 1 diff --git a/fixtures/prod-boot/configuration.yaml b/fixtures/prod-boot/configuration.yaml new file mode 100644 index 0000000..5f79014 --- /dev/null +++ b/fixtures/prod-boot/configuration.yaml @@ -0,0 +1,18 @@ +App\Entity\Configuration: + configuration_template (template): + type: !php/enum App\Enum\ConfigurationType::INSTANCE + + features (extends configuration_template): + configuration: + notificationsSender: + notificationsSenderEmail: info@example.com + notificationsSenderName: Contact + contact: + contactFormEnabled: false + contactFormEmail: info@example.com + groups: + groupsEnabled: false + groupsCreationMode: !php/const App\Message\Command\Admin\ParametersFormCommand::ONLY_ADMIN + groupsPaying: false + confidentiality: + confidentialityConversationAdminAccess: false diff --git a/fixtures/prod-boot/menu.yaml b/fixtures/prod-boot/menu.yaml new file mode 100644 index 0000000..cb4d1d6 --- /dev/null +++ b/fixtures/prod-boot/menu.yaml @@ -0,0 +1,5 @@ +App\Entity\Menu: + menu: + code: 'menu' + footer: + code: 'footer' diff --git a/fixtures/prod-boot/page.yaml b/fixtures/prod-boot/page.yaml new file mode 100644 index 0000000..1477195 --- /dev/null +++ b/fixtures/prod-boot/page.yaml @@ -0,0 +1,20 @@ +App\Entity\Page: + # —— Templates ————————————————————————————————————————————————————————————— + page_template (template): + enabled: true + + home: + name: Accueil + content: | + \

Plateforme EBS

+

Bienvenue sur Plateforme EBS !

+

+ + + +

+ + cgu: + name: CGU + content: | + \

CGU

diff --git a/fixtures/prod-boot/user.yaml b/fixtures/prod-boot/user.yaml new file mode 100644 index 0000000..7d19843 --- /dev/null +++ b/fixtures/prod-boot/user.yaml @@ -0,0 +1,17 @@ +App\Entity\User: + base_user_template (template): + enabled: true + mainAdminAccount: false + password: '\$2y\$13\$LOIpgrMmOysCysIwkILTl.qD8psPxn9U9/V03p3odlqztLb7Aewze' # 35DVDj8ir3Buc7 + emailConfirmed: true + + admin_template (template, extends base_user_template): + type: !php/enum App\Enum\User\UserType::ADMIN + roles: [!php/const App\Entity\User::ROLE_ADMIN] + + # —— Main instance admin ⚡————————————————————————————————————————————————— + admin_main (extends admin_template): + email: 'plateforme-ebs@apes-hdf.org' + firstname: 'Main' + lastname: 'Admin' + mainAdminAccount: true diff --git a/fixtures/test/address.yaml b/fixtures/test/address.yaml new file mode 100644 index 0000000..98eb042 --- /dev/null +++ b/fixtures/test/address.yaml @@ -0,0 +1,49 @@ +App\Entity\Address: + address_template (template): + country: 'FR' + providedBy: 'nominatim' + attribution: 'Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright' + osmType: 'node' + + address_apes (template, extends address_template): + address: "235 boulevard Paul Painlevé" + addressSupplement: "Maison de l’Economie Sociale et Solidaire" + displayName: "APES, 235, Boulevard Paul Painlevé, Lille-Centre, Lille, Nord, Hauts-de-France, France métropolitaine, 59000, France" + latitude: 50.6241657 + longitude: 3.0809541 + streetNumber: "235" + streetName: "boulevard Paul Painlevé" + locality: "Lille" + postalCode: "59000" + osmId: 4401366511 + + address_region_hauts_de_france (extends address_template): + address: '151 Avenue du président Hoover' + displayName: 'Région Hauts-de-France, 151, Avenue du président Hoover, Lille, Nord, Hauts-de-France, France métropolitaine, 59555, France' + streetNumber: '151' + streetName: 'Avenue du président Hoover' + locality: 'Lille' + postalCode: '59555' + latitude: 50.6297225 + longitude: 3.0777683 + osmId: 6576374057 + + address_loic (extends address_apes): + id: + addressSupplement: '3ème étage' + + address_camille (extends address_apes): + + address_user_17 (extends address_apes): + + address_prefecture_hauts_de_france (extends address_template): + address: "12 rue Jean-Sans-Peur" + displayName: "Préfecture de région - Hauts-de-France, 12, Rue Jean-Sans-Peur, Lille, Nord, Hauts-de-France, France métropolitaine, 59800, France" + latitude: 50.6326446 + longitude: 3.0585815 + streetNumber: "12" + streetName: "Rue Jean-Sans-Peur" + subLocality: "Fives" + locality: "Lille" + postalCode: "59800" + osmId: 2694662910 diff --git a/fixtures/test/category.yaml b/fixtures/test/category.yaml new file mode 100644 index 0000000..40949da --- /dev/null +++ b/fixtures/test/category.yaml @@ -0,0 +1,102 @@ +App\Entity\Category: + # —— Templates ————————————————————————————————————————————————————————————— + category_template (template): + enabled: true + + category_object (template, extends category_template): + type: !php/enum App\Enum\Product\ProductType::OBJECT + + category_service (template, extends category_template): + type: !php/enum App\Enum\Product\ProductType::SERVICE + + # —— Objects ——————————————————————————————————————————————————————————————— + category_object_1 (extends category_object): + id: + name: Activités manuelles et créatives + + category_object_2 (extends category_object): + name: Cartes & Guides de voyage + + category_object_3 (extends category_object): + name: Cuisine & accessoires + + category_object_4 (extends category_object): + name: Culture - jeux & instruments de musique + + category_service_4_0 (extends category_object): + parent: '@category_object_4' + name: Guitare + + category_object_5 (extends category_object): + name: Entretien de la maison + + category_object_6 (extends category_object): + name: Equipements pour véhicules + + category_object_7 (extends category_object): + name: Images & son + + category_object_8 (extends category_object): + name: Informatique - téléphonie consoles et jeux vidéos + + category_object_9 (extends category_object): + name: Jardinage + + category_object_10 (extends category_object): + name: Livres + + category_object_11 (extends category_object): + name: Loisirs divers + + category_object_12 (extends category_object): + name: Matériel de sports + + category_object_13 (extends category_object): + name: Matériel de vacances et weekend + + category_object_14 (extends category_object): + name: Matériel médical + + category_object_15 (extends category_object): + name: Puériculture + + category_object_16 (extends category_object): + name: Réception & fêtes + + category_object_17 (extends category_object): + name: Travaux & outillage + + category_object_18 (extends category_object): + id: + name: Vélos & accessoires + + category_object_19 (extends category_object): + name: Vêtements - accessoires - Beauté + + category_object_not_visible (extends category_object): + name: Pas visible sur le front + enabled: false + + # —— Services —————————————————————————————————————————————————————————————— + category_service_1 (extends category_service): + id: + name: Aide bricolage + + category_service_2 (extends category_service): + name: Cours + + category_service_2_0 (extends category_service): + id: + parent: '@category_service_2' + name: Cours de musique + + category_service_2_1 (extends category_service): + parent: '@category_service_2' + name: Cours d'échecs + + category_service_2_2 (extends category_service): + parent: '@category_service_2' + name: Autres cours + + category_service_3 (extends category_service): + name: Soutien scolaire diff --git a/fixtures/test/configuration.yaml b/fixtures/test/configuration.yaml new file mode 100644 index 0000000..0fbb4b4 --- /dev/null +++ b/fixtures/test/configuration.yaml @@ -0,0 +1,18 @@ +App\Entity\Configuration: + configuration_template (template): + type: !php/enum App\Enum\ConfigurationType::INSTANCE + + features (extends configuration_template): + configuration: + notificationsSender: + notificationsSenderEmail: info@example.com + notificationsSenderName: Contact + contact: + contactFormEnabled: true + contactFormEmail: info@example.com + groups: + groupsEnabled: true + groupsCreationMode: !php/const App\Message\Command\Admin\ParametersFormCommand::ALL + groupsPaying: true + confidentiality: + confidentialityConversationAdminAccess: true diff --git a/fixtures/test/group.yaml b/fixtures/test/group.yaml new file mode 100644 index 0000000..3800ee2 --- /dev/null +++ b/fixtures/test/group.yaml @@ -0,0 +1,81 @@ +App\Entity\Group: + group_template (template): + + group_1 (extends group_template): + id: + name: Groupe 1 + description: > + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean velit erat, + fringilla sit amet dui in, vehicula ultrices dui. + url: https://example.com/ + membership: !php/enum App\Enum\Group\GroupMembership::CHARGED + invitation_by_admin: true + + group_2 (extends group_template): + name: Groupe 2 + description: > + Mauris varius vitae nunc ac maximus. Aliquam quis placerat nisi. + Pellentesque aliquam magna sed elit interdum, sed elementum erat posuere. + url: https://example.com/ + membership: !php/enum App\Enum\Group\GroupMembership::FREE + + group_3 (extends group_template): + name: Groupe 3 + description: Sed aliquet mauris egestas vehicula consequat. + url: https://example.com/ + membership: !php/enum App\Enum\Group\GroupMembership::CHARGED + + group_4 (extends group_template): + name: Groupe 4 + description: Fusce ut ante ut libero dictum varius quis vel ante. Nulla facilisi. + url: https://example.com/ + membership: !php/enum App\Enum\Group\GroupMembership::CHARGED + + group_5 (extends group_template): + id: + name: Groupe 5 + description: > + Integer consectetur semper est, eu euismod dolor laoreet eget. + Aliquam a convallis turpis. Aliquam ultricies lectus ut fringilla consequat. + url: https://example.com/ + + group_6 (extends group_template): + name: Groupe 6 + description: Phasellus scelerisque eleifend lobortis. Mauris et diam sit amet nisl hendrerit convallis vel ac tellus. + url: https://example.com/ + + group_7 (extends group_template): + id: + name: Groupe 7 + description: > + Nullam id lacus libero. Integer luctus, dui in tempus mattis, justo tellus aliquam eros, id facilisis dolor mi id tellus. + Sed lobortis sapien felis, vel ultricies lorem consectetur commodo. + url: https://example.com/ + membership: !php/enum App\Enum\Group\GroupMembership::CHARGED + invitation_by_admin: true + + # —— End of real groups ———————————————————————————————————————————————————— + + # private group + group_private (extends group_template): + id: + name: Groupe privé + type: !php/enum App\Enum\Group\GroupType::PRIVATE + description: Aliquam interdum viverra sem et cursus. + invitation_by_admin: true + + # group with children + group_parent: + id: + name: Groupe parent + description: > + Duis gravida sapien et nunc sollicitudin, ac tempor erat ullamcorper. + membership: !php/enum App\Enum\Group\GroupMembership::FREE + + # group with a parent + group_child: + name: Groupe enfant + parent: '@group_parent' + description: > + Aenean mattis arcu nec arcu tempor mattis. + membership: !php/enum App\Enum\Group\GroupMembership::FREE diff --git a/fixtures/test/group_offer.yaml b/fixtures/test/group_offer.yaml new file mode 100644 index 0000000..723a1f4 --- /dev/null +++ b/fixtures/test/group_offer.yaml @@ -0,0 +1,84 @@ +App\Entity\GroupOffer: + group_offer_template (template): + + # Templates + group_offer_group_1_template (template, extends group_offer_template): + group: '@group_1' + type: !php/enum App\Enum\Group\GroupOfferType::YEARLY + + group_offer_group_7_template (template, extends group_offer_template): + group: '@group_7' + type: !php/enum App\Enum\Group\GroupOfferType::YEARLY + + # Group 1 + group_offer_group_1_10 (extends group_offer_group_1_template): + id: + name: Lorem ipsum + price: 1000 + + group_offer_group_1_20 (extends group_offer_group_1_template): + name: Dolor sit amet + price: 2000 + + group_offer_group_1_30 (extends group_offer_group_1_template): + name: Consectetur adipiscing + price: 3000 + + group_offer_group_1_40 (extends group_offer_group_1_template): + name: Suspendisse + price: 4000 + + group_offer_group_1_50 (extends group_offer_group_1_template): + name: Vivamus + price: 10000 + active: false + + # Group 7 + group_offer_group_7_min (extends group_offer_group_7_template): + name: Luctus viverra + price: 100 + + # error cases with magic amounts (https://docs.mollie.com/overview/testing) + group_offer_group_2_error1 (extends group_offer_group_7_template): + name: invalid_card_number + price: 100100 + + group_offer_group_2_error2 (extends group_offer_group_7_template): + name: invalid_cvv + price: 100200 + + group_offer_group_2_error3 (extends group_offer_group_7_template): + name: invalid_card_holder_name + price: 100300 + + group_offer_group_2_error4 (extends group_offer_group_7_template): + name: card_expired + price: 100400 + + group_offer_group_2_error5 (extends group_offer_group_7_template): + name: invalid_card_type + price: 100500 + + group_offer_group_2_error6 (extends group_offer_group_7_template): + name: refused_by_issuer + price: 100600 + + group_offer_group_2_error7 (extends group_offer_group_7_template): + name: insufficient_funds + price: 100700 + + group_offer_group_2_error8 (extends group_offer_group_7_template): + name: inactive_card + price: 100800 + + group_offer_group_2_error9 (extends group_offer_group_7_template): + name: possible_fraud + price: 100900 + + group_offer_group_2_error10 (extends group_offer_group_7_template): + name: authentication_failed + price: 101000 + + group_offer_group_2_error11 (extends group_offer_group_7_template): + name: card_declined + price: 101100 diff --git a/fixtures/test/menu.yaml b/fixtures/test/menu.yaml new file mode 100644 index 0000000..cb4d1d6 --- /dev/null +++ b/fixtures/test/menu.yaml @@ -0,0 +1,5 @@ +App\Entity\Menu: + menu: + code: 'menu' + footer: + code: 'footer' diff --git a/fixtures/test/menu_item.yaml b/fixtures/test/menu_item.yaml new file mode 100644 index 0000000..5898126 --- /dev/null +++ b/fixtures/test/menu_item.yaml @@ -0,0 +1,97 @@ +App\Entity\MenuItem: + menu_item (template): + menu: '@menu' + linkType: !php/enum App\Enum\Menu\LinkType::LINK + + footer_item (template): + menu: '@footer' + + footer_item_link (template, extends footer_item): + menu: '@footer' + linkType: !php/enum App\Enum\Menu\LinkType::LINK + + footer_item_social (template, extends footer_item): + menu: '@footer' + linkType: !php/enum App\Enum\Menu\LinkType::SOCIAL_NETWORK + + # Menu items + menu_item_home (extends menu_item): # first position of first level header + id: + name: 'Accueil' + link: '/' + position: 0 + + menu_item_account (extends menu_item): + name: 'Compte' + link: '/fr/compte/creer-mon-compte' + position: 1 + + menu_item_create_account (extends menu_item): + name: 'Mon compte' + link: '/fr/mon-compte' + parent: '@menu_item_account' + position: 0 + + menu_item_forgotten_password (extends menu_item): + name: 'Mot de passe oublié' + link: '/fr/compte/mot-de-passe-oublie' + parent: '@menu_item_account' + position: 1 + + menu_item_logout (extends menu_item): + name: 'Se déconnecter' + link: '/logout' + parent: '@menu_item_account' + position: 2 + + menu_item_products (extends menu_item): + name: 'Produits' + link: '/fr/produits' + position: 2 + + menu_item_groups (extends menu_item): + name: 'Groupes' + link: '/fr/groupes' + position: 3 + + menu_item_cms_page_1 (extends menu_item): # last position of 1st level header + id: + name: 'Qui sommes nous ?' + link: '/fr/qui-sommes-nous' + position: 4 + + # Footer items: links (1st position of footer) + menu_item_footer_link1 (extends footer_item): + id: + name: 'APES Hauts-de-France' + link: 'http://www.apes-hdf.org' + + menu_item_footer_link2 (extends footer_item): + name: 'Plateformes coopératives' + link: 'https://apes-hdf.org/page-96-0-0.html' + + menu_item_footer_cgu (extends footer_item): + name: 'CGU' + link: '/fr/cgu' + + # Footer items: social + menu_item_footer_facebook (extends footer_item_social): + id: + link: 'https://www.facebook.com' + mediaType: !php/enum App\Enum\SocialMediaType::FACEBOOK + + menu_item_footer_mastodon (extends footer_item_social): + link: 'https://mastodon.online' + mediaType: !php/enum App\Enum\SocialMediaType::MASTODON + + menu_item_footer_tweeter (extends footer_item_social): + link: 'https://twitter.com' + mediaType: !php/enum App\Enum\SocialMediaType::TWITTER + + menu_item_footer_youtube (extends footer_item_social): + link: 'https://www.youtube.com' + mediaType: !php/enum App\Enum\SocialMediaType::YOUTUBE + + menu_item_footer_linkedin (extends footer_item_social): + link: 'https://www.linkedin.com' + mediaType: !php/enum App\Enum\SocialMediaType::LINKEDIN diff --git a/fixtures/test/messages.yaml b/fixtures/test/messages.yaml new file mode 100644 index 0000000..a926dd7 --- /dev/null +++ b/fixtures/test/messages.yaml @@ -0,0 +1,20 @@ +App\Entity\Message: + message (template): + serviceRequest: '@service_request_1' + + message_system_1 (extends message): + id: + type: !php/enum App\Enum\Message\MessageType::SYSTEM + messageTemplate: 'message.system.new' + messageParameters: + - '%recipient%': Adah + - '%startAt%': '01/02/2023' + - '%endAt%': '08/02/2023' + message: Début de la demande d'emprunt - Par Suzanne - Du 01/05/2023 au 08/05/2023. + + message_from_recipient_1 (extends message): + id: + type: !php/enum App\Enum\Message\MessageType::FROM_RECIPIENT + message: Bonjour. Je voudrais emprunter votre objet Vélo Fuji Jari 2.5. 🙂 + createdAt: + updatedAt: diff --git a/fixtures/test/page.yaml b/fixtures/test/page.yaml new file mode 100644 index 0000000..1eb920e --- /dev/null +++ b/fixtures/test/page.yaml @@ -0,0 +1,94 @@ +App\Entity\Page: + # —— Templates ————————————————————————————————————————————————————————————— + page_template (template): + enabled: true + home: false + + page_1 (extends page_template): + id: + name: Qui sommes-nous ? + content: | + \

Nulla pulvinar purus vel metus varius, nec dictum enim faucibus

+ +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Vestibulum vel ultrices enim. Cras maximus eget mauris eget elementum. + Nam id justo feugiat ipsum egestas imperdiet in eget erat. Phasellus felis mauris, ultricies eget felis eget, malesuada auctor nisl. + Aliquam aliquam lectus at tincidunt tristique. Nulla facilisis, ante vel egestas facilisis, ex odio accumsan urna, eu eleifend nisi dolor in erat. + Morbi orci augue, interdum eu faucibus non, vestibulum nec urna. Donec tristique lectus ligula, ac volutpat nisl commodo at. +

+ +

Pellentesque id facilisis dolor. Praesent libero enim, blandit non ipsum et, dapibus egestas dolor. + Etiam a dictum enim. Phasellus feugiat fringilla sollicitudin. +

+ +

Nullam eget ullamcorper arcu. Praesent in felis justo. Pellentesque feugiat at nisl et ornare. Integer nec imperdiet odio. + Fusce interdum scelerisque facilisis. Nulla rutrum mauris orci, at auctor elit laoreet eu. Mauris ut nulla lacus. + In quis commodo lacus, at accumsan libero. Duis at porta felis, a accumsan nibh. Donec non lorem non neque lacinia commodo nec eu tortor. + Fusce eu velit quis elit dignissim lobortis. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque sit amet justo quam. + Aliquam nec tortor facilisis, tristique massa non, ullamcorper justo. +

+ +

Sed tincidunt ut nunc lobortis bibendum. Suspendisse fermentum tincidunt quam vel vulputate. + Etiam nunc felis, sagittis nec ultrices sed, bibendum nec libero. Ut blandit nibh quam, vel ornare nisl blandit quis. + Curabitur malesuada quis mauris a sollicitudin. Sed vel nulla efficitur, commodo orci et, porttitor justo. + Cras semper magna scelerisque purus semper lacinia. Nam laoreet auctor eros ut porttitor. + Nunc id lacinia sem, eget vestibulum leo. Quisque pulvinar suscipit finibus. Morbi fringilla lobortis enim, et consectetur neque rutrum quis. +

+ +

Donec ut scelerisque mauris.

+ +
+ +

Lorem ipsum dolor sit amet.

+ +

Mauris augue velit, sollicitudin vel placerat vitae, tincidunt sed mauris. In scelerisque tempus quam at malesuada. + Proin sit amet erat ornare ante laoreet iaculis eu et magna. Quisque mi est, laoreet eu orci id, porta pulvinar elit. + Duis eu sagittis nisl, ut tempus orci. Phasellus tristique non sapien eu sodales.Vivamus id lectus leo. Nulla facilisi. + Nunc eleifend dui vitae augue consequat imperdiet. Mauris nec erat at metus ultricies finibus. Praesent fermentum, turpis pulvinar pulvinar euismod, + ligula tortor aliquam metus, a consequat neque ante a magna. Etiam luctus porta augue id pharetra. Maecenas sollicitudin vehicula dui, + nec bibendum libero euismod a. Mauris eu elementum nisi. In metus purus, consectetur luctus viverra sed, aliquet a libero. + Aenean vulputate elit quis urna molestie rhoncus. +

+ +

+ + home: + id: + name: Accueil + home: true + content: | + \

Nulla fermentum justo sed magna rutrum, eget fermentum dolor accumsan.

+ + cgu: + id: + name: CGU + content: | + \

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet nulla non mauris pharetra rhoncus ac non libero. Sed convallis mauris a blandit vehicula.

+ +

Nulla facilisi. In nunc metus, pretium eu fermentum in, scelerisque non tortor. Ut cursus risus ut nulla porta, eget mollis tellus accumsan. + Cras euismod est a dui fringilla, eget suscipit sapien lobortis. Sed erat purus, iaculis et libero vitae, interdum sollicitudin magna. Mauris vel feugiat dui.

+ +

Phasellus facilisis sit amet lectus a efficitur. Praesent auctor commodo libero vel scelerisque. Suspendisse commodo consectetur magna ut vestibulum. + Mauris quis turpis eget est lobortis ullamcorper. Duis sem arcu, posuere et consectetur ut, maximus ut urna. + Nam vitae eleifend dolor. Quisque tempor eleifend tortor id eleifend. Pellentesque varius dolor scelerisque risus posuere euismod.

+ + +

Etiam eleifend pretium ex, vel euismod lorem ornare vel. Sed sit amet sodales ipsum. Vestibulum eget orci condimentum, commodo augue a, congue metus. + Aliquam nisi mi, malesuada quis justo sodales, sodales pellentesque nisi.

+ +

In hac habitasse platea dictumst. Aenean urna arcu, dignissim ut risus in, dignissim gravida mauris. Pellentesque luctus condimentum purus in finibus. Duis suscipit neque in augue mattis facilisis. + Phasellus sed euismod neque. In lacus lacus, interdum eget leo vitae, auctor egestas lectus. Nulla in nulla euismod, suscipit nulla ut, cursus nisl.

+ +

Vivamus malesuada a ante eget fermentum. Aliquam euismod vestibulum ipsum, pretium imperdiet risus lobortis et. Integer augue lacus, egestas at ultrices quis, eleifend in risus. + Proin suscipit a quam tempor imperdiet. In nisi sem, sodales sed commodo eget, ultrices vel felis.

+ +

Maecenas iaculis risus sed elit semper tempus. Curabitur maximus dapibus rhoncus. Suspendisse nec metus eros. Etiam maximus leo ut sem tempus laoreet.

+ +

Le Site permet aux Adhérents de diffuser leurs offres de Prêts d’objets. + L’Adhérent s’engage à ne proposer en Prêt que des objets dont il est propriétaire et en bon état, + et à fournir tous renseignements utiles facilitant son usage par l’Adhérent emprunteur tel que mode d’emploi, + date de disponibilité, lieu choisi pour le retrait et sa restitution, nombre de Points attendus.

+ +

Vivamus rutrum varius erat. Donec porttitor a purus quis tincidunt. Duis porttitor sodales lobortis. Maecenas tincidunt elit velit. Maecenas consequat urna diam, non porta est consequat vitae. Cras lobortis eu risus at facilisis. + Cras tempus vehicula sapien, at vulputate dolor porttitor eu. Fusce et urna non sem laoreet condimentum.

+ diff --git a/fixtures/test/payment.yaml b/fixtures/test/payment.yaml new file mode 100644 index 0000000..4bb1922 --- /dev/null +++ b/fixtures/test/payment.yaml @@ -0,0 +1,27 @@ +App\Entity\Payment: + payment_template (template): + currencyCode: 'EUR' + + payment_user_16 (extends payment_template): + id: + user: '@user_16' + number: 'payum_64258ad56b1754.38811238' + description: > + Groupe 1 / Lorem ipsum + clientEmail: 'user8@example.com' + clientid: '1ed8d15a-fa38-6f54-9188-f15f4a44bdf2' + totalAmount: 1000 + details: + method: creditcard + paid: true + status: captured + metadata: + groupId: '1ed4bcca-336e-6732-a08c-a15bb85fa24a' + groupOfferId: '1edcf019-786a-6ba8-a77d-598378ee78e4' + amount: 1000 + currency: 'EUR' + number: 'payum_64259610892935.12964644' + description: > + Groupe 1 / Lorem ipsum + client_email: 'user16@example.com' + client_id: '1edae1d3-f66a-6f68-8057-41b63a425612' diff --git a/fixtures/test/product.yaml b/fixtures/test/product.yaml new file mode 100644 index 0000000..3a0fc0a --- /dev/null +++ b/fixtures/test/product.yaml @@ -0,0 +1,190 @@ +App\Entity\Product: + product (template): + status: !php/enum App\Enum\Product\ProductStatus::ACTIVE + visibility: !php/enum App\Enum\Product\ProductVisibility::PUBLIC + + object (template, extends product): + type: !php/enum App\Enum\Product\ProductType::OBJECT + + service (template, extends product): + type: !php/enum App\Enum\Product\ProductType::SERVICE + + # Loic ————————————————————————————————————————————————————————————————————— + loic_object_1 (extends object): + id: + owner: '@admin_loic' + category: '@category_object_18' + name: Vélo Fuji Jari 2.5 + description: > + Très beau vélo Fuji Jari 2.5. + + Taille Cadre 54. + age: Acheté neuf en septembre 2022. + deposit: 3000 + images: ["4437be7d-ce40-43f0-99b4-4adddcc3316f.jpg"] + + # Paused object + loic_object_2 (extends object): + id: + status: !php/enum App\Enum\Product\ProductStatus::PAUSED + owner: '@admin_loic' + category: '@category_object_8' + name: Carte vidéo externe AMD Radeon Pro 380 + description: > + Pour pouvoir jouer dans de bonnes conditions (Pour Mac Mini). + Prété à mon frère en ce moment (sans le site). + age: Acheté début 2019. + deposit: 1000 + preferredLoanDuration: 1 mois mininum svp. + + user_16_object_3 (extends object): + owner: '@admin_loic' + category: '@category_service_4_0' + name: Guitare électrique + description: > + Guitare en bon état. + age: 2010 + + loic_service_1 (extends service): + id: + owner: '@admin_loic' + category: '@category_service_2_1' + name: Cours d'échecs initiation + description: > + Initiation aux échecs pour débutants (1 heure). + duration: > + Une ou deux heures si besoin. + + # Kevin ——————————————————————————————————————————————————————————————————— + kevin_object_1 (extends object): + id: + owner: '@admin_kevin' + category: '@category_object_17' + name: Diable + description: > + Très bien pour les déménagements, même proches. + age: Acheté en 2012. + + kevin_object_2 (extends object): + owner: '@admin_kevin' + category: '@category_object_17' + name: Perceuse + description: > + Très bien pour percer des trous dans la maison. + age: Acheté en 2015. + + # Camille ————————————————————————————————————————————————————————————————————— + camille_object_1 (extends object): + owner: '@admin_camille' + category: '@category_object_9' + name: Tondeuse à pelouse + description: > + Très belle Tondeuse pour avoir de l'herbe bien courte. + age: 2016 + deposit: 5000 + + camille_object_2 (extends object): + owner: '@admin_camille' + category: '@category_object_11' + name: Une paire de jumelles + description: > + Très belle paire de jumelles pour bien voir au loin. + age: 2017 + deposit: 1000 + + camille_object_3 (extends object): + owner: '@admin_camille' + category: '@category_object_18' + name: Vélo électrique + description: > + Plus facile pour les longues distances. + age: 2022 + deposit: 2000 + + camille_object_4 (extends object): + owner: '@admin_camille' + category: '@category_object_18' + name: Remorque à vélo + description: > + Pour transporter son enfant ou son chat. + age: 2022 + deposit: 5000 + + # user 16 ————————————————————————————————————————————————————————————————————— + user_16_service_1 (extends service): + id: + owner: '@user_16' + category: '@category_service_1' + name: Aide bricolage + description: > + Petits travaux de peinture et bricolage diverses. + duration: Une journée. + + user_16_object_1 (extends object): + id: + owner: '@user_16' + category: '@category_object_4' + name: Guitare électrique + description: > + Très belle guitare électrique en bon état. + age: 2013 + deposit: 2000 + + user_16_object_2 (extends object): + owner: '@user_16' + category: '@category_object_4' + name: Piano + description: > + Piano en bon état. + age: 2010 + + # place apes ————————————————————————————————————————————————————————————————————— + place_apes_service_1 (extends service): + owner: '@place_apes' + category: '@category_service_2' + name: Cours de chant + description: > + Pour débutants, amateurs ou artistes professionnels. + duration: Une journée. + + # Sarah ————————————————————————————————————————————————————————————————————— + place_sarah_service_1 (extends service): + owner: '@admin_sarah' + category: '@category_service_2_0' + name: Cours de piano + description: > + Pour débutants, amateurs ou artistes professionnels. + duration: Une heure. + + place_sarah_service_2 (extends service): + owner: '@admin_sarah' + category: '@category_service_2_2' + name: Cours d'histoire + description: > + Cours de rattrapage en histoire, aide aux devoirs, remise à niveau. + duration: > + Une journée. + + # Place 6 ——————————————————————————————————————————————————————————————————— + place_6_object_1 (extends object): + owner: '@place_6' + category: '@category_object_1' + name: Machine à coudre + description: > + Machine à coudre ( pas fournie avec les bobines de fil). + duration: Une journée. + age: De 2 à 10 ans + deposit: 0 + + place_6_object_2 (extends object): + id: + owner: '@place_6' + category: '@category_object_11' + name: Boule à Facette + description: > + Boule à facette avec moteur rotation et spot directionnel + duration: Une journée. + age: Moins de 2 ans + visibility: !php/enum App\Enum\Product\ProductVisibility::RESTRICTED + groups: + - '@group_1' diff --git a/fixtures/test/product_availability.yaml b/fixtures/test/product_availability.yaml new file mode 100644 index 0000000..3549c12 --- /dev/null +++ b/fixtures/test/product_availability.yaml @@ -0,0 +1,34 @@ +App\Entity\ProductAvailability: + product_availability (template): + mode: !php/enum App\Enum\Product\ProductAvailabilityMode::UNAVAILABLE + + product_availability_user (template, extends product_availability): + type: !php/enum App\Enum\Product\ProductAvailabilityType::OWNER + + product_availability_service_request (template, extends product_availability): + type: !php/enum App\Enum\Product\ProductAvailabilityType::SERVICE_REQUEST + + # —— Ongoing service request ——————————————————————————————————————————————— + product_availability_object_sr_1 (extends product_availability_service_request): + product: '@loic_object_1' + serviceRequest: '@service_request_1' + startAt: '@service_request_1->startAt' + endAt: '@service_request_1->endAt' + + # —— Owner black list —————————————————————————————————————————————————————— + product_availability_user_1 (extends product_availability_user): + id: + product: '@loic_object_1' + startAt: '' + endAt: '' + + product_availability_2_user_1 (extends product_availability_user): + product: '@loic_object_1' + startAt: '' + endAt: '' + + product_availability_object_sr_2 (extends product_availability_service_request): + product: '@kevin_object_1' + serviceRequest: '@service_request_2' + startAt: '@service_request_2->startAt' + endAt: '@service_request_2->endAt' diff --git a/fixtures/test/service_request.yaml b/fixtures/test/service_request.yaml new file mode 100644 index 0000000..95e6c00 --- /dev/null +++ b/fixtures/test/service_request.yaml @@ -0,0 +1,67 @@ +App\Entity\ServiceRequest: + service_request (template): + status: !php/enum App\Enum\ServiceRequest\ServiceRequestStatus::NEW + + # will start tomorrow + # see ServiceRequestStatusWorkflowControllerRefuseTest + service_request_1 (extends service_request): + id: + owner: '@admin_loic' + product: '@loic_object_1' + recipient: '@user_17' + startAt: '' + endAt: '' + + # ongoing service request, can be finalized manually + # @see ServiceRequestStatusWorkflowControllerFinalizeTest + service_request_2 (extends service_request): + id: + status: !php/enum App\Enum\ServiceRequest\ServiceRequestStatus::CONFIRMED + owner: '@admin_kevin' + product: '@kevin_object_1' + recipient: '@admin_loic' + startAt: '' + endAt: '' + + # can be auto-finalized + # @see ServiceRequestStatusWorkflowControllerAutoFinalizeTest + service_request_3 (extends service_request): + id: + status: !php/enum App\Enum\ServiceRequest\ServiceRequestStatus::CONFIRMED + owner: '@admin_loic' + product: '@loic_service_1' + recipient: '@admin_camille' + startAt: '' + endAt: '' + + # can be confirmed by the recipient + # @see ServiceRequestStatusWorkflowModifyRecipientTest + service_request_4 (extends service_request): + id: + status: !php/enum App\Enum\ServiceRequest\ServiceRequestStatus::TO_CONFIRM + owner: '@place_6' + product: '@place_6_object_1' + recipient: '@admin_loic' + startAt: '' + endAt: '' + + # can be confirmed by the recipient + service_request_5 (extends service_request): + id: + status: !php/enum App\Enum\ServiceRequest\ServiceRequestStatus::TO_CONFIRM + owner: '@admin_loic' + product: '@loic_object_1' + recipient: '@user_16' + startAt: '' + endAt: '' + + # ongoing service request that is about to start and end + # it finishes the same day to test noth end and start notifications with the + # same record + service_request_6 (extends service_request): + status: !php/enum App\Enum\ServiceRequest\ServiceRequestStatus::CONFIRMED + owner: '@admin_camille' + product: '@camille_object_1' + recipient: '@user_16' + startAt: '' + endAt: '' diff --git a/fixtures/test/user.yaml b/fixtures/test/user.yaml new file mode 100644 index 0000000..8f41e95 --- /dev/null +++ b/fixtures/test/user.yaml @@ -0,0 +1,173 @@ +App\Entity\User: + base_user_template (template): + enabled: true + mainAdminAccount: false + password: '\$2y\$13\$LOIpgrMmOysCysIwkILTl.qD8psPxn9U9/V03p3odlqztLb7Aewze' # 35DVDj8ir3Buc7 + emailConfirmed: true + phoneNumber: '+33600000000' + smsNotifications: true + + admin_template (template, extends base_user_template): + type: !php/enum App\Enum\User\UserType::ADMIN + roles: [!php/const App\Entity\User::ROLE_ADMIN] + + user_template (template, extends base_user_template): + type: !php/enum App\Enum\User\UserType::USER + + place_template (template, extends base_user_template): + type: !php/enum App\Enum\User\UserType::PLACE + + # —— Admins ———————————————————————————————————————————————————————————————— + admin_camille (extends admin_template): + id: + email: 'camille@example.com' + firstname: 'Camille' + lastname: 'Croteau' + mainAdminAccount: true + address: '@address_camille' + + admin_loic (extends admin_template): + id: + email: 'loic@example.com' + firstname: 'Loïc' + lastname: 'Duclos' + devAccount: true + address: '@address_loic' + + admin_kevin (extends admin_template): + id: + email: 'kevin@example.com' + firstname: 'Kevin' + lastname: 'Pirouet' + + admin_apes (extends admin_template): + id: + address: '@address_loic' + email: 'plateformcoop@apes-hdf.org' + firstname: 'APES' + lastname: 'APES' + + admin_sarah (extends admin_template): + id: + email: 'sarah@example.com' + firstname: 'Sarah' + lastname: 'Charest' + devAccount: true + address: '@address_region_hauts_de_france' + category: '@category_object_16' + + # —— Places ———————————————————————————————————————————————————————————————— + place_{6} (extends place_template): + email: 'lieu\@example.com' + name: 'Lieu n°' + schedule: '9h30 - 17h30' + + place_{7} (extends place_template): + id: + email: 'lieu\@example.com' + name: 'Lieu n°' + schedule: '9h - 17h' + + place_apes (extends place_template): + id: + email: 'compte+lieu@apes-hdf.org' + name: 'APES compte lieu' + address: '@address_region_hauts_de_france' + schedule: '9h30 - 17h30' + phoneNumber: null + + # —— Users ————————————————————————————————————————————————————————————————— + # user with vacation mode + user_{9} (extends user_template): + id: + email: 'user\@example.com' + firstname: + lastname: + vacationMode: true + + # deactivated user + user_{10} (extends user_template): + id: + email: 'user\@example.com' + firstname: + lastname: + enabled: false + + # confirmed user with a pending invitation + user_{11} (extends user_template): + id: + email: 'user\@example.com' + firstname: + lastname: + emailConfirmed: true + + # user ready to access the account creation step 2 + user_{12} (extends user_template): + id: + password: null + emailConfirmed: true + email: 'user\@example.com' + confirmationToken: '3PpTWgYdgNZcuRTbqZTS5HRihEGGhw5rCszuo7XYAPJ9dEwttR' + confirmationExpiresAt: + firstname: + lastname: + + # user with an expired confirmation token + # user with an unconfirmed email + user_{13} (extends user_template): + id: + password: null + emailConfirmed: false + email: 'user\@example.com' + confirmationToken: 'DrCaEPr3pKM9e8PkfUZiZZsAe5nwcgBDpQjKbuaJ3ukzL5qLv9' + confirmationExpiresAt: + firstname: + lastname: + + # user with a valid lost password token and a confirmation token for an invitation + user_{14} (extends user_template): + id: + password: null + emailConfirmed: false + email: 'user\@example.com' + lostPasswordToken: 'cuYxfS5eCWX2FYtJwWdhHZrGY6W1KT7UBV6CeARK2E2s4V3SKB' + lostPasswordExpiresAt: + confirmationToken: 'cuYxfS5eCWX2FYtJwWdhHZrGY6W1KT7UBV6CeARK2E2s4V3SKB' + confirmationExpiresAt: + firstname: + lastname: + + # user with an expired lost password token + user_{15} (extends user_template): + id: + password: null + emailConfirmed: false + email: 'user\@example.com' + lostPasswordToken: 'A4QJZqhf3wFnoJCf65xLwce2f7aMWkLEoZHshvHCDWC61vQSAv' + lostPasswordExpiresAt: + firstname: + lastname: + + user_{16} (extends user_template): + id: + email: 'user\@example.com' + firstname: + lastname: + address: null + avatar: 'ba827ea0-b140-4cbf-9786-77ac980c648c.jpg' + + # user with an address and a preferred category set + user_{17} (extends user_template): + id: + email: 'user\@example.com' + firstname: + lastname: + category: '@category_object_1' + description: 'description example' + address: '@address_user_17' + + # Demo accounts + user_apes (extends user_template): + email: 'john.doe@example.com' + firstname: 'John' + lastname: 'Doe' diff --git a/fixtures/test/user_group.yaml b/fixtures/test/user_group.yaml new file mode 100644 index 0000000..2eddc14 --- /dev/null +++ b/fixtures/test/user_group.yaml @@ -0,0 +1,83 @@ +App\Entity\UserGroup: + # Group 7 —————————————————————————————————————————————————————————————— + loic_at_group_7: + id: + user: '@admin_loic' + group: '@group_7' + membership: !php/enum App\Enum\Group\UserMembership::MEMBER + + kevin_at_group_7: + user: '@admin_kevin' + group: '@group_7' + membership: !php/enum App\Enum\Group\UserMembership::ADMIN + + sarah_at_group_7: + user: '@admin_sarah' + group: '@group_7' + membership: !php/enum App\Enum\Group\UserMembership::MEMBER + startAt: + endAt: + payedAt: + + user11_invitation_at_group_7: + user: '@user_11' + group: '@group_7' + membership: !php/enum App\Enum\Group\UserMembership::INVITATION + + # Group 1 ——————————————————————————————————————————————————————————————————— + camille_at_group_1: + user: '@admin_camille' + group: '@group_1' + membership: !php/enum App\Enum\Group\UserMembership::MEMBER + payedAt: + startAt: + endAt: + + user_16_at_group_1: + user: '@user_16' + group: '@group_1' + membership: !php/enum App\Enum\Group\UserMembership::ADMIN + + # membership expired + sarah_at_group_1: + user: '@admin_sarah' + group: '@group_1' + membership: !php/enum App\Enum\Group\UserMembership::MEMBER + mainAdminAccount: true + payedAt: + startAt: + endAt: + + place_apes_at_group_1: + user: '@place_apes' + group: '@group_1' + membership: !php/enum App\Enum\Group\UserMembership::ADMIN + + user14_invitation_at_group_1: + user: '@user_14' + group: '@group_1' + membership: !php/enum App\Enum\Group\UserMembership::INVITATION + + user_apes_admin_at_group_1: + user: '@user_apes' + group: '@group_1' + membership: !php/enum App\Enum\Group\UserMembership::ADMIN + mainAdminAccount: true + + user11_invitation_at_group_private: + user: '@user_11' + group: '@group_private' + membership: !php/enum App\Enum\Group\UserMembership::INVITATION + + # Private group ———————————————————————————————————————————————————————————— + sarah_at_group_private: + user: '@admin_sarah' + group: '@group_private' + membership: !php/enum App\Enum\Group\UserMembership::MEMBER + + # Group 2 —————————————————————————————————————————————————————————————— + sarah_invitation_at_group_2: + user: '@admin_sarah' + group: '@group_2' + membership: !php/enum App\Enum\Group\UserMembership::INVITATION + diff --git a/helm/chart/.gitignore b/helm/chart/.gitignore new file mode 100644 index 0000000..80bf7fc --- /dev/null +++ b/helm/chart/.gitignore @@ -0,0 +1 @@ +charts \ No newline at end of file diff --git a/helm/chart/.helmignore b/helm/chart/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/chart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/chart/Chart.lock b/helm/chart/Chart.lock new file mode 100644 index 0000000..f626756 --- /dev/null +++ b/helm/chart/Chart.lock @@ -0,0 +1,18 @@ +dependencies: +- name: postgresql + repository: https://charts.bitnami.com/bitnami/ + version: 11.9.13 +- name: external-dns + repository: https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami + version: 5.4.16 +- name: redis + repository: https://charts.bitnami.com/bitnami/ + version: 17.4.3 +- name: meilisearch + repository: https://meilisearch.github.io/meilisearch-kubernetes + version: 0.1.46 +- name: maildev + repository: https://pando85.github.io/helm-maildev/ + version: 0.3.1 +digest: sha256:282a6d90f79ac02f80158c66f22939298fa1dfba29ecd4d2885e55597c515500 +generated: "2023-02-01T09:52:52.035346504+01:00" diff --git a/helm/chart/Chart.yaml b/helm/chart/Chart.yaml new file mode 100644 index 0000000..df55e68 --- /dev/null +++ b/helm/chart/Chart.yaml @@ -0,0 +1,48 @@ +apiVersion: v2 +name: plateformcoop-ebs +description: A Helm chart for the PlateformCoop-EBS project +# home: https://plateformcoop-ebs.dev +# icon: https://plateformcoop-ebs.dev/logo-250x250.png + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.0.1 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +appVersion: 0.0.1 + +dependencies: + # bitnami chart are using the workaround from https://github.com/bitnami/charts/issues/10539 + - name: postgresql + version: ~11.9.13 + repository: https://charts.bitnami.com/bitnami/ + condition: postgresql.enabled + - name: external-dns + version: ~5.4.15 + repository: https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami + condition: external-dns.enabled + - name: redis + version: ~17.4.0 + repository: https://charts.bitnami.com/bitnami/ + condition: redis.enabled + - name: meilisearch + version: ~0.1.46 + repository: https://meilisearch.github.io/meilisearch-kubernetes + condition: meilisearch.enabled + - name: maildev + version: ~0.3.1 + repository: https://pando85.github.io/helm-maildev/ + condition: maildev.enabled diff --git a/helm/chart/README.md b/helm/chart/README.md new file mode 100644 index 0000000..00aa363 --- /dev/null +++ b/helm/chart/README.md @@ -0,0 +1,21 @@ +# Chart HELM + +to test locally with [minikube](https://minikube.sigs.k8s.io/docs/) + +```bash +minikube start +minikube addons enable ingress +kubectx minikube +kubectl create ns plateformcoop-ebs +kubens plateformcoop-ebs +``` + +get minikube ip via `minikube ip` + +add in your `/etc/hosts` file: + +``` +192.168.x.x ebs.chart-example.local maildev.chart-example.local +``` + +Then run `./test_minikube.sh` to build prod images, push them to minikube and deploy the app with helm diff --git a/helm/chart/templates/NOTES.txt b/helm/chart/templates/NOTES.txt new file mode 100644 index 0000000..873860f --- /dev/null +++ b/helm/chart/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "plateformcoop-ebs" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "plateformcoop-ebs" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "plateformcoop-ebs" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "plateformcoop-ebs.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/chart/templates/_helpers.tpl b/helm/chart/templates/_helpers.tpl new file mode 100644 index 0000000..2d0c2c3 --- /dev/null +++ b/helm/chart/templates/_helpers.tpl @@ -0,0 +1,92 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "plateformcoop-ebs.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "plateformcoop-ebs" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "plateformcoop-ebs.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "plateformcoop-ebs.labels" -}} +helm.sh/chart: {{ include "plateformcoop-ebs.chart" . }} +{{ include "plateformcoop-ebs.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Common labels PWA +*/}} +{{- define "plateformcoop-ebs.labelsPWA" -}} +helm.sh/chart: {{ include "plateformcoop-ebs.chart" . }} +{{ include "plateformcoop-ebs.selectorLabelsPWA" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "plateformcoop-ebs.selectorLabels" -}} +app.kubernetes.io/name: {{ include "plateformcoop-ebs.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/part-of: {{ include "plateformcoop-ebs.name" . }} +{{- end }} + +{{/* +Selector labels PWA +*/}} +{{- define "plateformcoop-ebs.selectorLabelsPWA" -}} +app.kubernetes.io/name: {{ include "plateformcoop-ebs.name" . }}-pwa +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/part-of: {{ include "plateformcoop-ebs.name" . }} +{{- end }} + +{{/* +Selector labels Fixtures job +*/}} +{{- define "plateformcoop-ebs.selectorLabelsFixtures" -}} +app.kubernetes.io/name: {{ include "plateformcoop-ebs.name" . }}-pwa +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "plateformcoop-ebs.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "plateformcoop-ebs" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/chart/templates/configmap.yaml b/helm/chart/templates/configmap.yaml new file mode 100644 index 0000000..2d8a088 --- /dev/null +++ b/helm/chart/templates/configmap.yaml @@ -0,0 +1,28 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "plateformcoop-ebs" . }} + labels: + {{- include "plateformcoop-ebs.labels" . | nindent 4 }} +data: + payum-gateway: {{ .Values.payum.gateway | quote }} + php-host: {{ .Values.php.host | quote }} + php-app-env: {{ .Values.php.appEnv | quote }} + php-app-debug: {{ .Values.php.appDebug | quote }} + php-cors-allow-origin: {{ .Values.php.corsAllowOrigin | quote }} + php-trusted-hosts: {{ .Values.php.trustedHosts | quote }} + php-trusted-proxies: "{{ join "," .Values.php.trustedProxies }}" + mercure-url: "http://{{ include "plateformcoop-ebs" . }}/.well-known/mercure" + mercure-public-url: {{ .Values.mercure.publicUrl | default "http://127.0.0.1/.well-known/mercure" | quote }} + mercure-extra-directives: {{ .Values.mercure.extraDirectives | quote }} + {{- if .Values.meilisearch.enabled }} + meilisearch-url: "http://{{ include "meilisearch.fullname" .Subcharts.meilisearch }}:7700" + {{- end }} + {{- if .Values.redis.enabled }} + redis-url: "redis://{{ printf "%s-master" (include "common.names.fullname" .Subcharts.redis) }}" + {{- end }} + payum-gateway: {{ .Values.payum.gateway | quote }} + php-storage-bucket: {{ .Values.php.storage.bucket | quote }} + php-storage-endpoint: {{ .Values.php.storage.endpoint | quote }} + php-storage-region: {{ .Values.php.storage.region | quote }} + php-storage-use-path-style-endpoint: {{ .Values.php.storage.usePathStyleEndpoint | quote }} \ No newline at end of file diff --git a/helm/chart/templates/cronjobs/cronjobEndMembership.yaml b/helm/chart/templates/cronjobs/cronjobEndMembership.yaml new file mode 100644 index 0000000..5bef8b7 --- /dev/null +++ b/helm/chart/templates/cronjobs/cronjobEndMembership.yaml @@ -0,0 +1,148 @@ +{{- if .Values.dailyCronjobs.enabled }} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "plateformcoop-ebs" . }}-cronjob-end-membership + labels: + {{- include "plateformcoop-ebs.labels" . | nindent 4 }} +spec: + schedule: '6 1 * * *' + jobTemplate: + metadata: + annotations: + rollme: {{ randAlphaNum 5 | quote }} + labels: + {{- include "plateformcoop-ebs.selectorLabels" . | nindent 8 }} + spec: + template: + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 12 }} + {{- end }} + serviceAccountName: {{ include "plateformcoop-ebs.serviceAccountName" . }} + restartPolicy: Never + containers: + - name: {{ .Chart.Name }}-cronjob-end-membership + image: "{{ .Values.php.image.repository }}:{{ .Values.php.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.php.image.pullPolicy }} + command: ['/bin/sh', '-c'] + args: ['bin/console app:end-membership --env=prod'] + env: + - name: API_ENTRYPOINT_HOST + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-host + - name: JWT_PASSPHRASE + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-passphrase + - name: JWT_PUBLIC_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-public-key + - name: JWT_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-secret-key + - name: TRUSTED_HOSTS + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-trusted-hosts + - name: TRUSTED_PROXIES + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-trusted-proxies + - name: APP_ENV + value: "prod" + - name: APP_DEBUG + value: "0" + - name: APP_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-app-secret + - name: CORS_ALLOW_ORIGIN + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-cors-allow-origin + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: database-url + - name: MERCURE_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-url + - name: MERCURE_PUBLIC_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-public-url + - name: MERCURE_JWT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-jwt-secret + - name: STORAGE_BUCKET + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-bucket + - name: STORAGE_ENDPOINT + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-endpoint + - name: STORAGE_REGION + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-region + - name: STORAGE_USE_PATH_STYLE_ENDPOINT + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-use-path-style-endpoint + - name: STORAGE_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-key + - name: STORAGE_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-secret + lifecycle: + preStop: + exec: + command: ["/bin/sh", "-c", "/bin/sleep 1; kill -QUIT 1"] + startupProbe: + exec: + command: + - docker-healthcheck + failureThreshold: 40 + periodSeconds: 3 + readinessProbe: + exec: + command: + - docker-healthcheck + periodSeconds: 3 + livenessProbe: + exec: + command: + - docker-healthcheck + periodSeconds: 3 + resources: + {{- toYaml .Values.resources.fixtures | nindent 16 }} +{{- end }} diff --git a/helm/chart/templates/cronjobs/cronjobNotifyMembershipExpiration1.yaml b/helm/chart/templates/cronjobs/cronjobNotifyMembershipExpiration1.yaml new file mode 100644 index 0000000..c783b9a --- /dev/null +++ b/helm/chart/templates/cronjobs/cronjobNotifyMembershipExpiration1.yaml @@ -0,0 +1,148 @@ +{{- if .Values.dailyCronjobs.enabled }} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "plateformcoop-ebs" . }}-cronjob-notify-ms-e-1 + labels: + {{- include "plateformcoop-ebs.labels" . | nindent 4 }} +spec: + schedule: '12 2 * * *' + jobTemplate: + metadata: + annotations: + rollme: {{ randAlphaNum 5 | quote }} + labels: + {{- include "plateformcoop-ebs.selectorLabels" . | nindent 8 }} + spec: + template: + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 12 }} + {{- end }} + serviceAccountName: {{ include "plateformcoop-ebs.serviceAccountName" . }} + restartPolicy: Never + containers: + - name: {{ .Chart.Name }}-cronjob-notify-ms-e-1 + image: "{{ .Values.php.image.repository }}:{{ .Values.php.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.php.image.pullPolicy }} + command: ['/bin/sh', '-c'] + args: ['bin/console app:notify-membership-expiration 1 --env=prod'] + env: + - name: API_ENTRYPOINT_HOST + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-host + - name: JWT_PASSPHRASE + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-passphrase + - name: JWT_PUBLIC_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-public-key + - name: JWT_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-secret-key + - name: TRUSTED_HOSTS + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-trusted-hosts + - name: TRUSTED_PROXIES + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-trusted-proxies + - name: APP_ENV + value: "prod" + - name: APP_DEBUG + value: "0" + - name: APP_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-app-secret + - name: CORS_ALLOW_ORIGIN + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-cors-allow-origin + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: database-url + - name: MERCURE_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-url + - name: MERCURE_PUBLIC_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-public-url + - name: MERCURE_JWT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-jwt-secret + - name: STORAGE_BUCKET + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-bucket + - name: STORAGE_ENDPOINT + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-endpoint + - name: STORAGE_REGION + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-region + - name: STORAGE_USE_PATH_STYLE_ENDPOINT + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-use-path-style-endpoint + - name: STORAGE_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-key + - name: STORAGE_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-secret + lifecycle: + preStop: + exec: + command: ["/bin/sh", "-c", "/bin/sleep 1; kill -QUIT 1"] + startupProbe: + exec: + command: + - docker-healthcheck + failureThreshold: 40 + periodSeconds: 3 + readinessProbe: + exec: + command: + - docker-healthcheck + periodSeconds: 3 + livenessProbe: + exec: + command: + - docker-healthcheck + periodSeconds: 3 + resources: + {{- toYaml .Values.resources.fixtures | nindent 16 }} +{{- end }} diff --git a/helm/chart/templates/cronjobs/cronjobNotifyMembershipExpiration7.yaml b/helm/chart/templates/cronjobs/cronjobNotifyMembershipExpiration7.yaml new file mode 100644 index 0000000..400b9e5 --- /dev/null +++ b/helm/chart/templates/cronjobs/cronjobNotifyMembershipExpiration7.yaml @@ -0,0 +1,148 @@ +{{- if .Values.dailyCronjobs.enabled }} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "plateformcoop-ebs" . }}-cronjob-notify-ms-e-7 + labels: + {{- include "plateformcoop-ebs.labels" . | nindent 4 }} +spec: + schedule: '3 21 * * *' + jobTemplate: + metadata: + annotations: + rollme: {{ randAlphaNum 5 | quote }} + labels: + {{- include "plateformcoop-ebs.selectorLabels" . | nindent 8 }} + spec: + template: + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 12 }} + {{- end }} + serviceAccountName: {{ include "plateformcoop-ebs.serviceAccountName" . }} + restartPolicy: Never + containers: + - name: {{ .Chart.Name }}-cronjob-notify-ms-e-7 + image: "{{ .Values.php.image.repository }}:{{ .Values.php.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.php.image.pullPolicy }} + command: ['/bin/sh', '-c'] + args: ['bin/console app:notify-membership-expiration 7 --env=prod'] + env: + - name: API_ENTRYPOINT_HOST + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-host + - name: JWT_PASSPHRASE + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-passphrase + - name: JWT_PUBLIC_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-public-key + - name: JWT_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-secret-key + - name: TRUSTED_HOSTS + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-trusted-hosts + - name: TRUSTED_PROXIES + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-trusted-proxies + - name: APP_ENV + value: "prod" + - name: APP_DEBUG + value: "0" + - name: APP_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-app-secret + - name: CORS_ALLOW_ORIGIN + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-cors-allow-origin + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: database-url + - name: MERCURE_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-url + - name: MERCURE_PUBLIC_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-public-url + - name: MERCURE_JWT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-jwt-secret + - name: STORAGE_BUCKET + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-bucket + - name: STORAGE_ENDPOINT + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-endpoint + - name: STORAGE_REGION + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-region + - name: STORAGE_USE_PATH_STYLE_ENDPOINT + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-use-path-style-endpoint + - name: STORAGE_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-key + - name: STORAGE_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-secret + lifecycle: + preStop: + exec: + command: ["/bin/sh", "-c", "/bin/sleep 1; kill -QUIT 1"] + startupProbe: + exec: + command: + - docker-healthcheck + failureThreshold: 40 + periodSeconds: 3 + readinessProbe: + exec: + command: + - docker-healthcheck + periodSeconds: 3 + livenessProbe: + exec: + command: + - docker-healthcheck + periodSeconds: 3 + resources: + {{- toYaml .Values.resources.fixtures | nindent 16 }} +{{- end }} diff --git a/helm/chart/templates/cronjobs/cronjobNotifyServiceRequestDatesEnd.yaml b/helm/chart/templates/cronjobs/cronjobNotifyServiceRequestDatesEnd.yaml new file mode 100644 index 0000000..8431875 --- /dev/null +++ b/helm/chart/templates/cronjobs/cronjobNotifyServiceRequestDatesEnd.yaml @@ -0,0 +1,148 @@ +{{- if .Values.dailyCronjobs.enabled }} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "plateformcoop-ebs" . }}-cronjob-notify-srq-end + labels: + {{- include "plateformcoop-ebs.labels" . | nindent 4 }} +spec: + schedule: '44 4 * * *' + jobTemplate: + metadata: + annotations: + rollme: {{ randAlphaNum 5 | quote }} + labels: + {{- include "plateformcoop-ebs.selectorLabels" . | nindent 8 }} + spec: + template: + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 12 }} + {{- end }} + serviceAccountName: {{ include "plateformcoop-ebs.serviceAccountName" . }} + restartPolicy: Never + containers: + - name: {{ .Chart.Name }}-cronjob-notify-srq-end + image: "{{ .Values.php.image.repository }}:{{ .Values.php.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.php.image.pullPolicy }} + command: ['/bin/sh', '-c'] + args: ['bin/console app:notify-service-request-dates end --env=prod'] + env: + - name: API_ENTRYPOINT_HOST + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-host + - name: JWT_PASSPHRASE + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-passphrase + - name: JWT_PUBLIC_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-public-key + - name: JWT_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-secret-key + - name: TRUSTED_HOSTS + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-trusted-hosts + - name: TRUSTED_PROXIES + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-trusted-proxies + - name: APP_ENV + value: "prod" + - name: APP_DEBUG + value: "0" + - name: APP_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-app-secret + - name: CORS_ALLOW_ORIGIN + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-cors-allow-origin + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: database-url + - name: MERCURE_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-url + - name: MERCURE_PUBLIC_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-public-url + - name: MERCURE_JWT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-jwt-secret + - name: STORAGE_BUCKET + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-bucket + - name: STORAGE_ENDPOINT + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-endpoint + - name: STORAGE_REGION + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-region + - name: STORAGE_USE_PATH_STYLE_ENDPOINT + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-use-path-style-endpoint + - name: STORAGE_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-key + - name: STORAGE_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-secret + lifecycle: + preStop: + exec: + command: ["/bin/sh", "-c", "/bin/sleep 1; kill -QUIT 1"] + startupProbe: + exec: + command: + - docker-healthcheck + failureThreshold: 40 + periodSeconds: 3 + readinessProbe: + exec: + command: + - docker-healthcheck + periodSeconds: 3 + livenessProbe: + exec: + command: + - docker-healthcheck + periodSeconds: 3 + resources: + {{- toYaml .Values.resources.fixtures | nindent 16 }} +{{- end }} diff --git a/helm/chart/templates/cronjobs/cronjobNotifyServiceRequestDatesStart.yaml b/helm/chart/templates/cronjobs/cronjobNotifyServiceRequestDatesStart.yaml new file mode 100644 index 0000000..d362f62 --- /dev/null +++ b/helm/chart/templates/cronjobs/cronjobNotifyServiceRequestDatesStart.yaml @@ -0,0 +1,148 @@ +{{- if .Values.dailyCronjobs.enabled }} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "plateformcoop-ebs" . }}-cronjob-notify-srq-start + labels: + {{- include "plateformcoop-ebs.labels" . | nindent 4 }} +spec: + schedule: '2 4 * * *' + jobTemplate: + metadata: + annotations: + rollme: {{ randAlphaNum 5 | quote }} + labels: + {{- include "plateformcoop-ebs.selectorLabels" . | nindent 8 }} + spec: + template: + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 12 }} + {{- end }} + serviceAccountName: {{ include "plateformcoop-ebs.serviceAccountName" . }} + restartPolicy: Never + containers: + - name: {{ .Chart.Name }}-cronjob-notify-srq-start + image: "{{ .Values.php.image.repository }}:{{ .Values.php.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.php.image.pullPolicy }} + command: ['/bin/sh', '-c'] + args: ['bin/console app:notify-service-request-dates start --env=prod'] + env: + - name: API_ENTRYPOINT_HOST + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-host + - name: JWT_PASSPHRASE + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-passphrase + - name: JWT_PUBLIC_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-public-key + - name: JWT_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-secret-key + - name: TRUSTED_HOSTS + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-trusted-hosts + - name: TRUSTED_PROXIES + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-trusted-proxies + - name: APP_ENV + value: "prod" + - name: APP_DEBUG + value: "0" + - name: APP_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-app-secret + - name: CORS_ALLOW_ORIGIN + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-cors-allow-origin + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: database-url + - name: MERCURE_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-url + - name: MERCURE_PUBLIC_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-public-url + - name: MERCURE_JWT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-jwt-secret + - name: STORAGE_BUCKET + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-bucket + - name: STORAGE_ENDPOINT + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-endpoint + - name: STORAGE_REGION + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-region + - name: STORAGE_USE_PATH_STYLE_ENDPOINT + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-use-path-style-endpoint + - name: STORAGE_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-key + - name: STORAGE_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-secret + lifecycle: + preStop: + exec: + command: ["/bin/sh", "-c", "/bin/sleep 1; kill -QUIT 1"] + startupProbe: + exec: + command: + - docker-healthcheck + failureThreshold: 40 + periodSeconds: 3 + readinessProbe: + exec: + command: + - docker-healthcheck + periodSeconds: 3 + livenessProbe: + exec: + command: + - docker-healthcheck + periodSeconds: 3 + resources: + {{- toYaml .Values.resources.fixtures | nindent 16 }} +{{- end }} diff --git a/helm/chart/templates/deployment.yaml b/helm/chart/templates/deployment.yaml new file mode 100644 index 0000000..de00972 --- /dev/null +++ b/helm/chart/templates/deployment.yaml @@ -0,0 +1,370 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "plateformcoop-ebs" . }} + labels: + {{- include "plateformcoop-ebs.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "plateformcoop-ebs.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "plateformcoop-ebs.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "plateformcoop-ebs.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + - name: {{ .Chart.Name }}-chown + securityContext: + runAsUser: 0 + image: "{{ .Values.php.image.repository }}:{{ .Values.php.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.php.image.pullPolicy }} + command: ['/bin/sh', '-c'] + args: ['set -ex; mkdir -p public/storage/uploads/category ; mkdir -p public/storage/uploads/menu ; mkdir -p public/storage/uploads/product ; mkdir -p public/storage/uploads/user ; chown -R www-data: public/storage/'] + volumeMounts: + - mountPath: /srv/app/public/storage + name: storage + containers: + - name: {{ .Chart.Name }}-caddy + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.caddy.image.repository }}:{{ .Values.caddy.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.caddy.image.pullPolicy }} + env: + - name: SERVER_NAME + value: :80 + - name: PWA_UPSTREAM + value: {{ include "plateformcoop-ebs" . }}-pwa:3000 + - name: MERCURE_EXTRA_DIRECTIVES + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-extra-directives + - name: MERCURE_PUBLISHER_JWT_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-jwt-secret + - name: MERCURE_SUBSCRIBER_JWT_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-jwt-secret + ports: + - name: http + containerPort: 80 + protocol: TCP + - name: admin + containerPort: 2019 + protocol: TCP + volumeMounts: + - mountPath: /srv/app/public/storage + name: storage + - mountPath: /var/run/php + name: php-socket + lifecycle: + preStop: + exec: + command: ["curl", "-XPOST", "http://localhost:2019/stop"] + readinessProbe: + tcpSocket: + port: 80 + initialDelaySeconds: 3 + livenessProbe: + tcpSocket: + port: 80 + initialDelaySeconds: 3 + resources: + {{- toYaml .Values.resources.caddy | nindent 12 }} + - name: {{ .Chart.Name }}-php + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.php.image.repository }}:{{ .Values.php.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.php.image.pullPolicy }} + env: + - name: API_ENTRYPOINT_HOST + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-host + - name: JWT_PASSPHRASE + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-passphrase + - name: JWT_PUBLIC_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-public-key + - name: JWT_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-secret-key + - name: TRUSTED_HOSTS + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-trusted-hosts + - name: TRUSTED_PROXIES + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-trusted-proxies + - name: APP_ENV + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-app-env + - name: APP_DEBUG + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-app-debug + - name: APP_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-app-secret + - name: CORS_ALLOW_ORIGIN + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-cors-allow-origin + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: database-url + - name: MAILER_DSN + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mailer-dsn + - name: MERCURE_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-url + - name: MERCURE_PUBLIC_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-public-url + - name: MERCURE_JWT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-jwt-secret + {{- if .Values.meilisearch.enabled }} + - name: MEILISEARCH_API_KEY + valueFrom: + secretKeyRef: + name: {{ printf "%s-%s" (include "meilisearch.fullname" .Subcharts.meilisearch ) "master-key" }} + key: MEILI_MASTER_KEY + - name: MEILISEARCH_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: meilisearch-url + {{- end }} + {{- if .Values.redis.enabled }} + - name: REDIS_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: redis-url + {{- end }} + - name: SMS_DSN + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: sms-dsn + - name: PAYUM_APIKEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: payum-apikey + - name: PAYUM_GATEWAY + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: payum-gateway + - name: STORAGE_BUCKET + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-bucket + - name: STORAGE_ENDPOINT + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-endpoint + - name: STORAGE_REGION + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-region + - name: STORAGE_USE_PATH_STYLE_ENDPOINT + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-use-path-style-endpoint + - name: STORAGE_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-key + - name: STORAGE_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-secret + volumeMounts: + - mountPath: /srv/app/public/storage + name: storage + - mountPath: /var/run/php + name: php-socket + lifecycle: + preStop: + exec: + command: ["/bin/sh", "-c", "/bin/sleep 1; kill -QUIT 1"] + startupProbe: + exec: + command: + - docker-healthcheck + failureThreshold: 40 + readinessProbe: + exec: + command: + - docker-healthcheck + livenessProbe: + exec: + command: + - docker-healthcheck + resources: + {{- toYaml .Values.resources.php | nindent 12 }} + {{- if .Values.consumer.enabled }} + - name: {{ .Chart.Name }}-consumer + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.php.image.repository }}:{{ .Values.php.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.php.image.pullPolicy }} + args: ['bin/console', 'messenger:consume'] + env: + - name: API_ENTRYPOINT_HOST + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-host + - name: JWT_PASSPHRASE + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-passphrase + - name: JWT_PUBLIC_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-public-key + - name: JWT_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-secret-key + - name: TRUSTED_HOSTS + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-trusted-hosts + - name: TRUSTED_PROXIES + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-trusted-proxies + - name: APP_ENV + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-app-env + - name: APP_DEBUG + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-app-debug + - name: APP_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-app-secret + - name: CORS_ALLOW_ORIGIN + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-cors-allow-origin + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: database-url + - name: MERCURE_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-url + - name: MERCURE_PUBLIC_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-public-url + - name: MERCURE_JWT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-jwt-secret + startupProbe: + exec: + command: ['pgrep', '-f', 'php bin/console messenger:consume'] + failureThreshold: 40 + readinessProbe: + exec: + command: ['pgrep', '-f', 'php bin/console messenger:consume'] + livenessProbe: + exec: + command: ['pgrep', '-f', 'php bin/console messenger:consume'] + resources: + {{- toYaml .Values.resources.consumer | nindent 12 }} + {{- end }} + volumes: + - name: storage + # TODO: pvc option? + emptyDir: {} + - name: php-socket + emptyDir: {} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/chart/templates/fixtures-job.yaml b/helm/chart/templates/fixtures-job.yaml new file mode 100644 index 0000000..d142b13 --- /dev/null +++ b/helm/chart/templates/fixtures-job.yaml @@ -0,0 +1,210 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "plateformcoop-ebs" . }}-fixtures + labels: + {{- include "plateformcoop-ebs.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": "post-install,post-upgrade" + "helm.sh/hook-delete-policy": "before-hook-creation" +spec: + template: + metadata: + annotations: + rollme: {{ randAlphaNum 5 | quote }} + labels: + {{- include "plateformcoop-ebs.selectorLabelsFixtures" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "plateformcoop-ebs.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + restartPolicy: Never + containers: + - name: {{ .Chart.Name }}-fixtures + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.php.image.repository }}:{{ .Values.php.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.php.image.pullPolicy }} + command: ['/bin/sh', '-c'] + args: [' + set -ex; + echo no fixtures job at the moment + '] + # if [ "$APP_ENV" != "prod" ]; then + # composer install --prefer-dist --no-progress --no-suggest --no-interaction; + # fi; + # bin/console doctrine:database:drop --if-exists --force; + # bin/console doctrine:database:create --if-not-exists; + # bin/console doctrine:schema:create; + # bin/console doctrine:schema:validate; + # bin/console messenger:setup-transports; + # bin/console hautelook:fixtures:load --no-interaction -vv --no-bundles; + # bin/console app:index-products; + env: + - name: API_ENTRYPOINT_HOST + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-host + - name: JWT_PASSPHRASE + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-passphrase + - name: JWT_PUBLIC_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-public-key + - name: JWT_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-jwt-secret-key + - name: TRUSTED_HOSTS + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-trusted-hosts + - name: TRUSTED_PROXIES + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-trusted-proxies + - name: APP_ENV + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-app-env + - name: APP_DEBUG + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-app-debug + - name: APP_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-app-secret + - name: CORS_ALLOW_ORIGIN + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-cors-allow-origin + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: database-url + - name: MERCURE_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-url + - name: MERCURE_PUBLIC_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-public-url + - name: MERCURE_JWT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: mercure-jwt-secret + {{- if .Values.meilisearch.enabled }} + - name: MEILISEARCH_API_KEY + valueFrom: + secretKeyRef: + name: {{ printf "%s-%s" (include "meilisearch.fullname" .Subcharts.meilisearch ) "master-key" }} + key: MEILI_MASTER_KEY + - name: MEILISEARCH_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: meilisearch-url + {{- end }} + {{- if .Values.redis.enabled }} + - name: REDIS_URL + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: redis-url + {{- end }} + - name: PAYUM_APIKEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: payum-apikey + - name: PAYUM_GATEWAY + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: payum-gateway + - name: STORAGE_BUCKET + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-bucket + - name: STORAGE_ENDPOINT + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-endpoint + - name: STORAGE_REGION + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-region + - name: STORAGE_USE_PATH_STYLE_ENDPOINT + valueFrom: + configMapKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-use-path-style-endpoint + - name: STORAGE_KEY + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-key + - name: STORAGE_SECRET + valueFrom: + secretKeyRef: + name: {{ include "plateformcoop-ebs" . }} + key: php-storage-secret + lifecycle: + preStop: + exec: + command: ["/bin/sh", "-c", "/bin/sleep 1; kill -QUIT 1"] + startupProbe: + exec: + command: + - docker-healthcheck + failureThreshold: 40 + periodSeconds: 3 + readinessProbe: + exec: + command: + - docker-healthcheck + periodSeconds: 3 + livenessProbe: + exec: + command: + - docker-healthcheck + periodSeconds: 3 + resources: + {{- toYaml .Values.resources.fixtures | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/chart/templates/hpa.yaml b/helm/chart/templates/hpa.yaml new file mode 100644 index 0000000..d62b748 --- /dev/null +++ b/helm/chart/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "plateformcoop-ebs" . }} + labels: + {{- include "plateformcoop-ebs.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "plateformcoop-ebs" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/chart/templates/ingress.yaml b/helm/chart/templates/ingress.yaml new file mode 100644 index 0000000..709664d --- /dev/null +++ b/helm/chart/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "plateformcoop-ebs" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "plateformcoop-ebs.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} diff --git a/helm/chart/templates/secrets.yaml b/helm/chart/templates/secrets.yaml new file mode 100644 index 0000000..10f4d72 --- /dev/null +++ b/helm/chart/templates/secrets.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "plateformcoop-ebs" . }} + labels: + {{- include "plateformcoop-ebs.labels" . | nindent 4 }} +type: Opaque +data: + {{- if .Values.postgresql.enabled }} + database-url: {{ printf "pgsql://%s:%s@%s-postgresql/%s?serverVersion=14&charset=utf8" .Values.postgresql.global.postgresql.auth.username .Values.postgresql.global.postgresql.auth.password .Release.Name .Values.postgresql.global.postgresql.auth.database | b64enc | quote }} + {{- else }} + database-url: {{ .Values.postgresql.url | b64enc | quote }} + {{- end }} + php-app-secret: {{ .Values.php.appSecret | default (randAlphaNum 40) | b64enc | quote }} + php-jwt-passphrase: {{ .Values.php.jwt.passphrase | b64enc | quote }} + php-jwt-public-key: {{ .Values.php.jwt.publicKey | b64enc | quote }} + php-jwt-secret-key: {{ .Values.php.jwt.secretKey | b64enc | quote }} + mercure-jwt-secret: {{ .Values.mercure.jwtSecret | default (randAlphaNum 40) | b64enc | quote }} + {{- if .Values.maildev.enabled }} + mailer-dsn: {{ printf "smtp://%s:%s" ( include "maildev.fullname" .Subcharts.maildev ) "1025" | b64enc | quote }} + {{- else }} + mailer-dsn: {{ .Values.mailer.dsn | b64enc | quote }} + {{- end }} + sms-dsn: {{ .Values.sms.dsn | b64enc | quote }} + payum-apikey: {{ .Values.payum.apikey | b64enc | quote }} + php-storage-key: {{ .Values.php.storage.key | b64enc | quote }} + php-storage-secret: {{ .Values.php.storage.secret | b64enc | quote }} \ No newline at end of file diff --git a/helm/chart/templates/service.yaml b/helm/chart/templates/service.yaml new file mode 100644 index 0000000..5cc67a1 --- /dev/null +++ b/helm/chart/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "plateformcoop-ebs" . }} + labels: + {{- include "plateformcoop-ebs.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "plateformcoop-ebs.selectorLabels" . | nindent 4 }} diff --git a/helm/chart/templates/serviceaccount.yaml b/helm/chart/templates/serviceaccount.yaml new file mode 100644 index 0000000..da060f6 --- /dev/null +++ b/helm/chart/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "plateformcoop-ebs.serviceAccountName" . }} + labels: + {{- include "plateformcoop-ebs.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/helm/chart/templates/tests/test-connection.yaml b/helm/chart/templates/tests/test-connection.yaml new file mode 100644 index 0000000..020d31e --- /dev/null +++ b/helm/chart/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "plateformcoop-ebs" . }}-test-connection" + labels: + {{- include "plateformcoop-ebs.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "plateformcoop-ebs" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/chart/test_minikube.sh b/helm/chart/test_minikube.sh new file mode 100755 index 0000000..acbf009 --- /dev/null +++ b/helm/chart/test_minikube.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +set -e +# go to repo root +cd $(dirname $0)/../../ + +# build all images, without dev overrides +docker compose -f docker-compose.yml build + +# tag image with sha to force rollingUpdate +php_sha=$(docker inspect plateformcoop-ebs-php --format='{{.Id}}' | cut -d: -f2) +caddy_sha=$(docker inspect plateformcoop-ebs-caddy --format='{{.Id}}' | cut -d: -f2) +docker tag plateformcoop-ebs-php plateformcoop-ebs-php:$php_sha +docker tag plateformcoop-ebs-caddy plateformcoop-ebs-caddy:$caddy_sha + +# push images to minikube +#minikube image load plateformcoop-ebs-php:$php_sha +#minikube image load plateformcoop-ebs-caddy:$caddy_sha +for image in plateformcoop-ebs-php:$php_sha plateformcoop-ebs-caddy:$caddy_sha; do + minikube image ls | grep $image || minikube image load $image +done + +# install or update deployment on minikube +helm upgrade --install demo ./helm/chart \ + --kube-context minikube \ + --namespace plateformcoop-ebs --create-namespace \ + --atomic \ + --wait \ + --debug \ + -f ./helm/chart/values-minikube.yml \ + --set php.image.tag=$php_sha \ + --set caddy.image.tag=$caddy_sha + +MINIKUBE_IP=$(minikube ip) +if ! grep -E "^$MINIKUBE_IP\s+(.+\s+)?ebs.chart-example.local" /etc/hosts; then + echo Execute \"echo $MINIKUBE_IP ebs.chart-example.local \| sudo tee -a /etc/hosts\" + exit=1 +fi +if ! grep -E "^$MINIKUBE_IP\s+(.+\s+)?maildev.chart-example.local" /etc/hosts; then + echo Execute \"echo $MINIKUBE_IP maildev.chart-example.local \| sudo tee -a /etc/hosts\" + exit=1 +fi + +if [ -n "$exit" ]; then + exit 1 +fi + +open http://ebs.chart-example.local +open http://maildev.chart-example.local diff --git a/helm/chart/values-minikube.yml b/helm/chart/values-minikube.yml new file mode 100644 index 0000000..5acd85e --- /dev/null +++ b/helm/chart/values-minikube.yml @@ -0,0 +1,44 @@ +meilisearch: + persistence: + enabled: true + size: "1Gi" + +redis: + master: + persistence: + enabled: true + size: "1Gi" + +payum: + enabled: true + gateway: 'mollie' + apikey: 'test' + +postgresql: + auth: + # PostgreSQL password is set only the first time chart in installed + postgresPassword: change_me + +maildev: + enabled: true + ingress: + enabled: true + hosts: + - maildev.chart-example.local + +php: + image: + repository: plateformcoop-ebs-php + tag: latest + storage: + bucket: "toto" + endpoint: "titi" + region: "tata" + usePathStyleEndpoint: true + key: "tonton" + secret: "tomtom" + +caddy: + image: + repository: plateformcoop-ebs-caddy + tag: latest diff --git a/helm/chart/values-prod.yml b/helm/chart/values-prod.yml new file mode 100644 index 0000000..3c10bd0 --- /dev/null +++ b/helm/chart/values-prod.yml @@ -0,0 +1,34 @@ +imagePullSecrets: + - name: regcred + +ingress: + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-production + hosts: + - host: toset + paths: + - path: / + pathType: Prefix + tls: + - secretName: toset + hosts: + - toset + +meilisearch: + persistence: + enabled: true + storageClass: "standard" + size: "1Gi" + +redis: + master: + persistence: + enabled: true + storageClass: "standard" + size: "1Gi" + +postgresql: + persistence: + enabled: true + storageClass: "standard" \ No newline at end of file diff --git a/helm/chart/values.yaml b/helm/chart/values.yaml new file mode 100644 index 0000000..9708d93 --- /dev/null +++ b/helm/chart/values.yaml @@ -0,0 +1,219 @@ +# Default values for plateformcoop-ebs. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +php: + image: + repository: "ghcr.io/ApesHDF/EBS-php" # CHANGE ME + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + appEnv: prod + appDebug: "0" + appSecret: "" + corsAllowOrigin: "^https?://.*?\\.chart-example\\.local$" + trustedHosts: "^127\\.0\\.0\\.1|localhost|.*\\.chart-example\\.local$" + trustedProxies: + - "127.0.0.1" + - "10.0.0.0/8" + - "172.16.0.0/12" + - "192.168.0.0/16" + host: "ebs.chart-example.local" + jwt: + secretKey: "" + publicKey: "" + passphrase: "" + storage: + bucket: "" + endpoint: "" + region: "" + usePathStyleEndpoint: true + publicKey: "" + secret: "" + +maildev: + enabled: false + +mailer: + # is set automaticaly if maildev is enabled + dsn: change_me + +sms: + dsn: "null://null" + +dailyCronjobs: + enabled: true + +consumer: + # We don't use async for now so consumer isn't needed + enabled: false + +caddy: + image: + repository: "ghcr.io/ApesHDF/EBS-caddy" # CHANGE ME + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +# You may prefer using the managed version in production: https://mercure.rocks +mercure: + publicUrl: https://ghcr.io/.well-known/mercure + # Change me! + jwtSecret: "!ChangeThisMercureHubJWTSecretKey!" + extraDirectives: cors_origins http://ghcr.io https://ghcr.io + +# Full configuration: https://github.com/bitnami/charts/tree/master/bitnami/postgresql +postgresql: + enabled: true + # If bringing your own PostgreSQL, the full uri to use + # url: postgresql://plateformcoop-ebs:!ChangeMe!@database:5432/api?serverVersion=13&charset=utf8 + global: + postgresql: + auth: + username: "example" + password: "!ChangeMe!" + database: "api" + # Persistent Volume Storage configuration. + # ref: https://kubernetes.io/docs/user-guide/persistent-volumes + pullPolicy: IfNotPresent + image: + repository: bitnami/postgresql + tag: 14 + primary: + persistence: + enabled: true + storageClass: standard + size: 1Gi + resources: + requests: + memory: 50Mi + cpu: 1m + +payum: + # @see https://my.mollie.com/dashboard/org_XXXXXXXX/developers/api-keys + # even it's a fake key it must start with 'test_' or 'live_' and must be at least 30 characters long + gateway: "mollie" + apikey: "test_FRabcdefghijklmnopqrstuvwxyzab" + +external-dns: + enabled: false + domainFilters: + - demo.local + provider: cloudflare + cloudflare: + apiToken: "" + zoneIdFilters: [] + rbac: + create: true + +# https://artifacthub.io/packages/helm/bitnami/redis +redis: + enabled: true + auth: + enabled: false + master: + startupProbe: + enabled: true + initialDelaySeconds: 1 + periodSeconds: 1 + livenessProbe: + initialDelaySeconds: 0 + readinessProbe: + initialDelaySeconds: 0 + replica: + replicaCount: 0 + +# https://github.com/meilisearch/meilisearch-kubernetes/blob/main/charts/meilisearch/README.md +meilisearch: + enabled: true + environment: + MEILI_ENV: 'production' + resources: + requests: + memory: 40Mi + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: 6m + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: ebs.chart-example.local + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: chart-example-tls + # hosts: + # - ghcr.io + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + php: + requests: + memory: 100Mi + caddy: + requests: + memory: 20Mi + consumer: + requests: + memory: 100Mi + fixtures: + requests: + memory: 100Mi + +# If you use Mercure, you need the managed or the On Premise version to deploy more than one pod: https://mercure.rocks/docs/hub/cluster +replicaCount: 1 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/migrations/.gitignore b/migrations/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/migrations/2023/Version20230103090224.php b/migrations/2023/Version20230103090224.php new file mode 100755 index 0000000..74e13bf --- /dev/null +++ b/migrations/2023/Version20230103090224.php @@ -0,0 +1,25 @@ +addSql('SELECT 1 as id'); + } + + public function down(Schema $schema): void + { + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6dd248e --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "devDependencies": { + "@babel/core": "^7.17.0", + "@babel/preset-env": "^7.16.0", + "@hotwired/stimulus": "^3.0.0", + "@popperjs/core": "^2.11.6", + "@symfony/stimulus-bridge": "^3.2.0", + "@symfony/ux-autocomplete": "file:vendor/symfony/ux-autocomplete/assets", + "@symfony/webpack-encore": "^4.0.0", + "bootstrap": "^5.2.3", + "bootstrap-icons": "^1.10.3", + "core-js": "^3.23.0", + "eslint": "^8.33.0", + "regenerator-runtime": "^0.13.9", + "sass": "^1.56.1", + "sass-loader": "^13.0.0", + "stimulus-carousel": "^5.0.1", + "stimulus-password-visibility": "^2.0.0", + "tom-select": "^2.2.2", + "webpack": "^5.74.0", + "webpack-cli": "^4.10.0", + "webpack-notifier": "^1.15.0" + }, + "license": "UNLICENSED", + "private": true, + "scripts": { + "dev-server": "encore dev-server", + "dev": "encore dev", + "watch": "encore dev --watch", + "build": "encore production --progress", + "lint": "eslint assets/ --ext .js" + }, + "dependencies": { + "@fortawesome/fontawesome-free": "^6.2.1", + "flatpickr": "^4.6.13" + } +} diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..c7664f9 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,24 @@ +#includes: + # https://github.com/spaze/phpstan-disallowed-calls + #- vendor/spaze/phpstan-disallowed-calls/disallowed-dangerous-calls.neon +# https://arnaud.le-blanc.net/post/phpstan-generics.html +# https://phpstan.org/blog/generics-in-php-using-phpdocs +parameters: + level: max + paths: + - config + - tests + - public + - src + symfony: + container_xml_path: var/cache/dev/App_KernelDevDebugContainer.xml + #bootstrapFiles: + #- %rootDir%/../../../vendor/twig/twig/src/Extension/CoreExtension.php + + # https://phpstan.org/config-reference#vague-typehints + checkMissingIterableValueType: true # https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type + + # https://phpstan.org/user-guide/ignoring-errors#reporting-unused-ignores + # reportUnmatchedIgnoredErrors: true + ignoreErrors: + # - '#foobar_pattern#' diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..a524fda --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + tests/ + + + + tests/Unit + + + + tests/Integration + + + + tests/Api + + + + tests/Functional + + + + tests/E2E + + + + + + src + + + + + + + + + + + diff --git a/public/.gitignore b/public/.gitignore new file mode 100644 index 0000000..3f549fa --- /dev/null +++ b/public/.gitignore @@ -0,0 +1 @@ +uploads diff --git a/public/apes.png b/public/apes.png new file mode 100644 index 0000000..87e1fe2 Binary files /dev/null and b/public/apes.png differ diff --git a/public/ckeditor.css b/public/ckeditor.css new file mode 100644 index 0000000..59ee6fb --- /dev/null +++ b/public/ckeditor.css @@ -0,0 +1,3 @@ +.cke_source { + color: black; +} diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..508a169 --- /dev/null +++ b/public/index.php @@ -0,0 +1,11 @@ + + + + + + diff --git a/public/svg/chat.svg b/public/svg/chat.svg new file mode 100644 index 0000000..64854b1 --- /dev/null +++ b/public/svg/chat.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/svg/funding.png b/public/svg/funding.png new file mode 100644 index 0000000..b9f4fa1 Binary files /dev/null and b/public/svg/funding.png differ diff --git a/public/svg/logo-black.svg b/public/svg/logo-black.svg new file mode 100644 index 0000000..0be538e --- /dev/null +++ b/public/svg/logo-black.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svg/logo.svg b/public/svg/logo.svg new file mode 100644 index 0000000..3258d11 --- /dev/null +++ b/public/svg/logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svg/placeholder.svg b/public/svg/placeholder.svg new file mode 100644 index 0000000..221761e --- /dev/null +++ b/public/svg/placeholder.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/svg/product.svg b/public/svg/product.svg new file mode 100644 index 0000000..40dc859 --- /dev/null +++ b/public/svg/product.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..8b0ae8f --- /dev/null +++ b/rector.php @@ -0,0 +1,16 @@ +import(SetList::PHP_81); + $rectorConfig->import(DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES); + $rectorConfig->import(SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES); + $rectorConfig->import(SensiolabsSetList::FRAMEWORK_EXTRA_61); +}; diff --git a/src/ApiResource/.gitignore b/src/ApiResource/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/ApiResource/GroupResource.php b/src/ApiResource/GroupResource.php new file mode 100644 index 0000000..ff0538e --- /dev/null +++ b/src/ApiResource/GroupResource.php @@ -0,0 +1,28 @@ + self::DESCRIPTION], + description: self::DESCRIPTION, + name: 'group_get_collection_stats', + provider: GroupGetStatsProvider::class + ), + ] +)] +class GroupResource +{ + final public const DESCRIPTION = 'Retrieve some stats from the group table.'; + + public int $count; +} diff --git a/src/Command/CommandTrait.php b/src/Command/CommandTrait.php new file mode 100644 index 0000000..36abbf7 --- /dev/null +++ b/src/Command/CommandTrait.php @@ -0,0 +1,46 @@ +info('Memory: '.round(memory_get_usage() / 1024 / 1024, 2)." mb\n"); + } + + private function done(SymfonyStyle $io): void + { + $io->success('DONE'); + } + + protected function configureCommand(string $description): void + { + [$desc, $class] = [$description, $this::class]; + $this + ->setHelp( + <<$class + +DEV: +%command.full_name% -vv + +PROD: +%command.full_name% --env=prod --no-debug +EOT + ); + } +} diff --git a/src/Command/EndMembershipCommand.php b/src/Command/EndMembershipCommand.php new file mode 100644 index 0000000..f75e1e4 --- /dev/null +++ b/src/Command/EndMembershipCommand.php @@ -0,0 +1,97 @@ +configureCommand(self::DESCRIPTION); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $io->title(self::DESCRIPTION.' ('.$this->environment.' env)'); + $this->memoryReport($io); + + $io->section('Getting concerned membership...'); + $query = $this->userGroupRepository->getExpired(); + + $io->section('Processing deletions...'); + $count = 0; + foreach ($query->toIterable() as $userGroup) { + /** @var UserGroup $userGroup */ + $user = $userGroup->getUser(); + $group = $userGroup->getGroup(); + $io->comment(sprintf(' > deleting membership for %s of %s (%s) (%s)', + $group->getName(), + $user->getDisplayName(), + $userGroup->getMembership()->value, + $user->getId() + )); + + // we could pass the UserGroup instance, but let's use the same command and handler for now + // As it isn't a user action, this command must put the product in vaction + // mode to avoid leaving products public as public without the user consent. + $quitGroupCommand = new QuitGroupCommand($group->getId(), $user->getId(), QuitGroupCommand::VACATION); + $this->commandBus->dispatch($quitGroupCommand); + $this->appMailer->send(EndMembershipEmail::class, compact('user', 'group')); + $this->sendSms($user, EndMembershipEmail::class, [ + '%group%' => $group->getName(), + ]); + ++$count; + } + + $io->note(sprintf(' > %d deletion(s) done.', $count)); + $this->memoryReport($io); + $this->done($io); + + return Command::SUCCESS; + } +} diff --git a/src/Command/NotifyMembershipExpirationCommand.php b/src/Command/NotifyMembershipExpirationCommand.php new file mode 100644 index 0000000..64a495b --- /dev/null +++ b/src/Command/NotifyMembershipExpirationCommand.php @@ -0,0 +1,98 @@ +configureCommand(self::DESCRIPTION); + $this->addArgument('days', InputArgument::REQUIRED, 'Number of days from tomorrow (1 = notifiy members expiring tomorrow)'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $io->title(self::DESCRIPTION.' ('.$this->environment.' env)'); + $this->memoryReport($io); + + /** @var string $days */ + $days = $input->getArgument('days'); + $days = max(1, (int) $days); + + $io->section(sprintf('Getting membership expiring in %d days...', $days)); + $query = $this->userGroupRepository->getExpiring($days); + $io->section('Sending notificaitons...'); + $count = 0; + foreach ($query->toIterable() as $userGroup) { + /** @var UserGroup $userGroup */ + $user = $userGroup->getUser(); + $group = $userGroup->getGroup(); + $io->comment(sprintf(' > notifying membership for %s of %s/%s (%s) (%s)', + $group->getName(), + $user->getDisplayName(), + $userGroup->getEndAt()?->format('Y-m-d'), + $userGroup->getMembership()->value, + $user->getId() + )); + + $this->appMailer->send(NotifyMembershipExpirationEmail::class, compact('user', 'group', 'days')); + $this->sendSms($user, EndMembershipEmail::class, [ + '%group%' => $group->getName(), + '%days' => $days, + ]); + ++$count; + } + + $io->note(sprintf(' > %d notification(s) sent.', $count)); + + $this->memoryReport($io); + $this->done($io); + + return Command::SUCCESS; + } +} diff --git a/src/Command/NotifyServiceRequestDatesCommand.php b/src/Command/NotifyServiceRequestDatesCommand.php new file mode 100644 index 0000000..bde2db3 --- /dev/null +++ b/src/Command/NotifyServiceRequestDatesCommand.php @@ -0,0 +1,116 @@ +configureCommand(self::DESCRIPTION); + $this->addArgument('mode', InputArgument::REQUIRED, 'If the notification is related to the startAt (value = start) date or endAt date (vakue = end) of the service request.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $io->title(self::DESCRIPTION.' ('.$this->environment.' env)'); + $this->memoryReport($io); + + /** @var string $mode */ + $mode = $input->getArgument('mode'); + $isStartMode = $mode === 'start'; + $io->note(' > mode : '.$mode); + $io->newLine(); + + $io->section('Getting services request...'); + $query = $isStartMode ? + $this->serviceRequestRepository->getStartingAtTomorow() : + $this->serviceRequestRepository->getEndingAtTomorow() + ; + $emailClass = $isStartMode ? + NotifyServiceRequestStartEmail::class : + NotifyServiceRequestEndEmail::class + ; + + $io->section('Sending notifications...'); + $count = 0; + foreach ($query->toIterable() as $serviceRequest) { + /** @var ServiceRequest $serviceRequest */ + $referenceDate = $isStartMode ? + $serviceRequest->getStartAt() : + $serviceRequest->getEndAt() + ; + $io->comment(sprintf(' > notifying owner and recipient for service request %s (%s) starting on %s.', + $serviceRequest->getId(), + $serviceRequest->getStatus()->value, + $referenceDate->format('Y-m-d') + )); + $context = [ + 'service_request' => $serviceRequest, + 'user' => $serviceRequest->getOwner(), + '%product%' => $serviceRequest->getProduct()->getName(), + '%date%' => $referenceDate->format($this->translator->trans('format.date', [], 'date')), + ]; + + $this->appMailer->send($emailClass, $context); + $this->sendSms($serviceRequest->getOwner(), $emailClass, $context); + + $context['user'] = $serviceRequest->getRecipient(); + $this->appMailer->send($emailClass, $context); + $this->sendSms($serviceRequest->getRecipient(), $emailClass, $context); + + ++$count; + } + + $io->note(sprintf(' > %d notification(s) sent.', $count * 2)); // owner and recipient + + $this->memoryReport($io); + $this->done($io); + + return Command::SUCCESS; + } +} diff --git a/src/Controller/.gitignore b/src/Controller/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/Controller/Admin/AbstractCategoryCrudController.php b/src/Controller/Admin/AbstractCategoryCrudController.php new file mode 100644 index 0000000..026929d --- /dev/null +++ b/src/Controller/Admin/AbstractCategoryCrudController.php @@ -0,0 +1,279 @@ +setEntityLabelInPlural($this->getEntityLabelInPlural()) + ->setEntityLabelInSingular($this->getEntityLabelInSingular()) + ->setSearchFields(['name']) + ->setFormThemes([ + '@EasyAdmin/crud/form_theme.html.twig', + 'easy_admin/crud/form_theme.html.twig', + ]) + ->setPaginatorPageSize(100) // display everyhting to have the while hierarchical view + ; + } + + public function configureFilters(Filters $filters): Filters + { + return $filters + ->add(UuidFilter::new('id')) + ->add('name') + ->add('parent') + ->add('enabled') + ; + } + + public function configureActions(Actions $actions): Actions + { + $moveDownAction = Action::new('down', $this->getI18nPrefix(self::class).'.menu.action.move_down', 'fa-sharp fa-solid fa-arrow-down') + ->linkToCrudAction('moveDown'); + $moveUpAction = Action::new('up', $this->getI18nPrefix(self::class).'.menu.action.move_up', 'fa-sharp fa-solid fa-arrow-up') + ->linkToCrudAction('moveUp'); + + // don't display the delete link if the item has children + $deleteAction = Action::new('delete', 'menu.action.delete') + ->linkToCrudAction('delete') + ->displayIf(static function (Category $category) { + return !$category->hasChildren(); + }) + ->setCssClass('dropdown-item action-delete text-danger'); + + return $actions + ->add(Crud::PAGE_INDEX, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::INDEX) + ->add(Crud::PAGE_INDEX, $moveDownAction) + ->add(Crud::PAGE_INDEX, $moveUpAction) + ->remove(Crud::PAGE_INDEX, Action::DELETE) + ->add(Crud::PAGE_INDEX, $deleteAction) + ; + } + + public static function getEntityFqcn(): string + { + return Category::class; + } + + public function getEntityLabelInSingular(): string + { + return 'category'; + } + + public function createEntity(string $entityFqcn): Category + { + /** @var Category $category */ + $category = new $entityFqcn(); + $category->setType($this->getCategoryType()); + + return $category; + } + + /** + * Only display a given type. + */ + public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder + { + $qb = parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters); + $alias = $qb->getRootAliases()[0] ?? null; + $qb->andWhere($alias.'.type = :type') + ->setParameter('type', $this->getCategoryType()) + ->orderBy($alias.'.lft', 'ASC') + ; + + return $qb; + } + + /** + * Return all possible fields. + * + * @return array + */ + public function getFields(string $pageName): array + { + $idField = IdField::new('id') + ->setLabel('id') + ->hideOnForm(); + + $typeField = ChoiceField::new('type') + ->setFormType(EnumType::class) + ->setFormTypeOption('class', ProductType::class) + ->setChoices(ProductType::getAsArray()); + + $parentField = AssociationField::new('parent') + ->setQueryBuilder(function (QueryBuilder $queryBuilder) { + return $this->categoryRepository->addTypeFilter($queryBuilder, $this->getCategoryType()); + }) + ->setLabel('category.parent') + ->setRequired(false) + ; + + $nameField = TextField::new('name') + ->formatValue(static function ($value, Category $category) { + return $category->getNameWithIndent(); + }); + + $enabledField = $this->getSimpleBooleanField('enabled'); + + $imageField = ImageField::new('image') + ->setLabel('image.default') + ->setHelp('image.help') + ->setBasePath($this->categoryBasePath) // correctly set the formatted value available in the template + ->setUploadDir('public'.$this->categoryBasePath) + ->setUploadedFileNamePattern('[uuid].[extension]') + ->setFormTypeOption('upload_new', $this->easyAdminHelper->getUploadNewCallback($this->categoryStorage)) + ->setFormTypeOption('upload_delete', $this->easyAdminHelper->getUploadDeleteCallback($this->categoryStorage)) + ->setFormTypeOption('constraints', $this->mediaManager->getFileConstraints()) + ->setTemplatePath('easy_admin/field/flysystem_image.html.twig') + ->setHelp($this->mediaManager->getHelpMessage()) + ; + + $createdAt = DateTimeField::new('createdAt'); + $updatedAt = DateTimeField::new('updatedAt'); + + return compact( + 'idField', + 'parentField', + 'typeField', + 'nameField', + 'enabledField', + 'imageField', + 'createdAt', + 'updatedAt' + ); + } + + public function configureFields(string $pageName): iterable + { + $panels = $this->getPanels(); + + [ + 'idField' => $idField, + 'typeField' => $typeField, + 'parentField' => $parentField, + 'nameField' => $nameField, + 'enabledField' => $enabledField, + 'imageField' => $imageField, + 'createdAt' => $createdAt, + 'updatedAt' => $updatedAt, + ] = $this->getFields($pageName); + + // list + if ($pageName === Crud::PAGE_INDEX) { + return [$nameField, $parentField, $enabledField, $imageField, $createdAt]; + } + + // forms + if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { + return [$nameField, $parentField, $enabledField, $imageField]; + } + + // detail + + return [ + $panels['information'], + $parentField, + $typeField, + $nameField, + $enabledField, + $imageField, + $panels['tech_information'], + $idField, + $createdAt, + $updatedAt, + ]; + } + + public function moveUp(AdminContext $context): Response + { + /** @var Category $item */ + $item = $context->getEntity()->getInstance(); + $this->categoryRepository->moveUp($item); + + return $this->redirectToObjectCrudPage(); + } + + public function moveDown(AdminContext $context): Response + { + /** @var Category $item */ + $item = $context->getEntity()->getInstance(); + $this->categoryRepository->moveDown($item); + + return $this->redirectToObjectCrudPage(); + } + + private function redirectToObjectCrudPage(): RedirectResponse + { + $this->addFlashSuccess($this->getI18nPrefix(self::class).'.move.success'); + $this->adminUrlGenerator + ->unsetAll() + ->setController($this->getCrudControllerClass()) + ->setAction('index'); + + return $this->redirect($this->adminUrlGenerator->generateUrl()); + } +} diff --git a/src/Controller/Admin/AbstractMenuCrudController.php b/src/Controller/Admin/AbstractMenuCrudController.php new file mode 100644 index 0000000..afc10cc --- /dev/null +++ b/src/Controller/Admin/AbstractMenuCrudController.php @@ -0,0 +1,168 @@ +setPageTitle(Crud::PAGE_EDIT, t($this->getPageTitle(), [], DashboardController::DOMAIN)) + ->setPageTitle(Crud::PAGE_DETAIL, t($this->getEntityLabelInSingular(), [], DashboardController::DOMAIN)) + ->setEntityLabelInPlural($this->getEntityLabelInPlural()) + ->setEntityLabelInSingular($this->getEntityLabelInSingular()) + ->setFormThemes([ + '@EasyAdmin/crud/form_theme.html.twig', + 'easy_admin/crud/form_theme.html.twig', + ]) + ; + } + + public function configureActions(Actions $actions): Actions + { + // Better button label for this kind of page + $actions->update(Crud::PAGE_EDIT, Action::SAVE_AND_CONTINUE, function (Action $action) { + return $action->setLabel('action.save'); + }); + + $itemsListUrl = $this->adminUrlGenerator + ->unsetAll() + ->setController($this->getMenuItemCrudControllerClass()) + ->set('crudAction', Crud::PAGE_INDEX) + ->set('menuIndex', $this->getMenuItemsIndex()) + ->generateUrl(); + + $itemsList = Action::new('editMenuItems', 'menu.action.items_list') + ->linkToUrl($itemsListUrl); + + return $actions + ->remove(Crud::PAGE_INDEX, Action::NEW) + ->remove(Crud::PAGE_INDEX, Action::DELETE) + ->remove(Crud::PAGE_DETAIL, Action::DELETE) + ->remove(Crud::PAGE_DETAIL, Action::INDEX) + ->remove(Crud::PAGE_EDIT, Action::SAVE_AND_RETURN) + ->add(Crud::PAGE_EDIT, Action::DETAIL) + ->add(Crud::PAGE_EDIT, $itemsList) + ; + } + + public static function getEntityFqcn(): string + { + return Menu::class; + } + + /** + * Return all possible fields. + * + * @return array + */ + public function getFields(string $pageName): array + { + $logoField = ImageField::new('logo') + ->setBasePath($this->menuBasePath) // correctly set the formatted value available in the template + ->setUploadDir('public'.$this->menuBasePath) + ->setUploadedFileNamePattern('[uuid].[extension]') + ->setFormTypeOption('upload_new', $this->easyAdminHelper->getUploadNewCallback($this->defaultStorage)) + ->setFormTypeOption('upload_delete', $this->easyAdminHelper->getUploadDeleteCallback($this->defaultStorage)) + ->setFormTypeOption('constraints', $this->mediaManager->getFileConstraints()) + ->setTemplatePath('easy_admin/field/flysystem_image.html.twig') + ->setHelp($this->mediaManager->getHelpMessage()) + ; + + $code = TextField::new('code'); + $items = CollectionField::new('items') + ->useEntryCrudForm(MenuItemCrudController::class) + ; + $itemsCount = IntegerField::new('itemsCount'); + $createdAt = DateTimeField::new('createdAt'); + $updatedAt = DateTimeField::new('updatedAt'); + + return compact( + 'logoField', + 'items', + 'itemsCount', + 'code', + 'createdAt', + 'updatedAt', + ); + } + + public function configureFields(string $pageName): iterable + { + $panels = $this->getPanels(); + + [ + 'logoField' => $logoField, + 'itemsCount' => $itemsCount, + 'code' => $code, + 'createdAt' => $createdAt, + 'updatedAt' => $updatedAt, + ] = $this->getFields($pageName); + + if ($pageName === Crud::PAGE_INDEX) { + return [$code, $logoField, $itemsCount, $createdAt, $updatedAt]; + } + + if ($pageName === Crud::PAGE_DETAIL) { + return [ + $panels['information'], + $logoField, + $itemsCount, + $panels['tech_information'], + $code, + $createdAt, + $updatedAt, + ]; + } + + // edit page + return [$logoField]; + } +} diff --git a/src/Controller/Admin/AbstractMenuItemCrudController.php b/src/Controller/Admin/AbstractMenuItemCrudController.php new file mode 100644 index 0000000..6bc428d --- /dev/null +++ b/src/Controller/Admin/AbstractMenuItemCrudController.php @@ -0,0 +1,284 @@ + 1, + Menu::FOOTER => 2, + ]; + + public function __construct( + private readonly AdminUrlGenerator $adminUrlGenerator, + private readonly MenuRepository $menuRepository, + private readonly MenuItemRepository $menuItemRepository, + ) { + } + + public function configureCrud(Crud $crud): Crud + { + return $crud + ->setEntityLabelInPlural($this->getEntityLabelInPlural()) + ->setEntityLabelInSingular($this->getEntityLabelInSingular()) + ->setDefaultSort(['parent' => 'DESC', 'position' => 'ASC']); + } + + public function configureFilters(Filters $filters): Filters + { + return $filters + ->add('name') + ->add('link') + ->add('parent') + ->add(EnumFilter::new('mediaType', SocialMediaTypeType::class)) + ; + } + + public function configureActions(Actions $actions): Actions + { + $editLogoUrl = $this->adminUrlGenerator + ->unsetAll() + ->setController($this->getMenuControllerClass()) + ->setEntityId(self::MENUS[$this->getCode()]) + ->set('crudAction', Crud::PAGE_EDIT) + ->generateUrl(); + + $editLogo = Action::new('editMenuItems', 'menu.action.edit_logo', 'fas fa-edit') + ->linkToUrl($editLogoUrl)->createAsGlobalAction(); + + $moveDownPosition = Action::new('down', 'menu.action.down_item', 'fa-sharp fa-solid fa-arrow-down') + ->linkToCrudAction('moveDownPosition'); + + $moveUpPosition = Action::new('up', 'menu.action.up_item', 'fa-sharp fa-solid fa-arrow-up') + ->linkToCrudAction('moveUpPosition') + ->displayIf(static function (MenuItem $item) { + return !$item->isFirst(); + }); + + // don't display the delete link if the item has children + $deleteAction = Action::new('delete', 'menu.action.delete') + ->linkToCrudAction('delete') + ->displayIf(static function (MenuItem $item) { + return !$item->hasChildren(); + }) + ->setCssClass('dropdown-item action-delete text-danger'); + + $newMenuItemLinkUrl = $this->adminUrlGenerator + ->unsetAll() + ->setController($this->getNewMenuItemLinkController()) + ->set('crudAction', Crud::PAGE_NEW) + ->generateUrl(); + + $newLinkGlobalAction = Action::new('link', 'icon.text') + ->linkToUrl($newMenuItemLinkUrl)->createAsGlobalAction(); + + $newMenuItemIconUrl = $this->adminUrlGenerator + ->unsetAll() + ->setController($this->getNewMenuItemIconController()) + ->set('crudAction', Crud::PAGE_NEW) + ->generateUrl(); + + $newMenuItemGlobalAction = Action::new('icon', 'icon.menu') + ->linkToUrl($newMenuItemIconUrl)->createAsGlobalAction(); + + return $actions + ->add(Crud::PAGE_INDEX, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::INDEX) + ->add(Crud::PAGE_INDEX, $editLogo) + ->add(Crud::PAGE_INDEX, $moveDownPosition) + ->add(Crud::PAGE_INDEX, $moveUpPosition) + ->add(Crud::PAGE_INDEX, $newLinkGlobalAction) + ->add(Crud::PAGE_INDEX, $newMenuItemGlobalAction) + ->remove(Crud::PAGE_INDEX, Action::NEW) + ->remove(Crud::PAGE_INDEX, Action::DELETE) + ->add(Crud::PAGE_INDEX, $deleteAction) + ; + } + + private function redirectToObjectCrudPage(bool $withFlash = true): RedirectResponse + { + if ($withFlash) { + $this->addFlash('success', 'menu_item.update_successful'); + } + $this->adminUrlGenerator + ->unsetAll() + ->setController($this->getMenuItemsControllerClass()) + ->setAction('index'); + + return $this->redirect($this->adminUrlGenerator->generateUrl()); + } + + public function moveUpPosition(AdminContext $context): Response + { + /** @var MenuItem $item */ + $item = $context->getEntity()->getInstance(); + $item->setPosition($item->up()); + $this->menuItemRepository->save($item, true); + + return $this->redirectToObjectCrudPage(); + } + + public function moveDownPosition(AdminContext $context): Response + { + /** @var MenuItem $item */ + $item = $context->getEntity()->getInstance(); + $oldPosition = $item->getPosition(); + $item->setPosition($item->down()); + $this->menuItemRepository->save($item, true); + $newPosition = $item->getPosition(); + + return $this->redirectToObjectCrudPage($newPosition !== $oldPosition); + } + + public static function getEntityFqcn(): string + { + return MenuItem::class; + } + + public function createEntity(string $entityFqcn): MenuItem + { + /** @var MenuItem $menuItem */ + $menuItem = new $entityFqcn(); + $menuItem->setMenu($this->getMenu()); + + return $menuItem; + } + + public function getMenu(): Menu + { + return $this->menuRepository->getByCode($this->getCode()); + } + + /** + * Only display menu items corresponding to a given menu. + */ + public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder + { + $qb = parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters); + $alias = $qb->getRootAliases()[0] ?? null; + $qb->andWhere($alias.'.menu = :menu') + ->setParameter('menu', $this->getMenu()); + + return $qb; + } + + /** + * Return all possible fields. + * + * @return array + */ + public function getFields(string $pageName): array + { + $linkTypeField = ChoiceField::new('linkType') + ->setFormType(EnumType::class) + ->setFormTypeOption('class', LinkType::class) + ->setChoices(LinkType::getAsArray()) + ; + $nameField = TextField::new('name') + ->setRequired(true); + $linkField = TextField::new('link'); + + $socialMediaTypeField = ChoiceField::new('mediaType') + ->setFormType(EnumType::class) + ->setFormTypeOption('class', SocialMediaType::class) + ->setChoices(SocialMediaType::getAsArray()) + ->setRequired(true); + + $parentField = AssociationField::new('parent') + ->setQueryBuilder(function (QueryBuilder $queryBuilder) { + return $this->menuItemRepository->getLinksByCode($queryBuilder, $this->getCode()); + }) + ->setRequired(false); + $menuField = AssociationField::new('menu'); + $positionField = IntegerField::new('position'); + $positionHumanField = IntegerField::new('positionHuman'); + + return compact( + 'linkTypeField', + 'nameField', + 'linkField', + 'socialMediaTypeField', + 'parentField', + 'menuField', + 'positionField', + 'positionHumanField', + ); + } + + public function configureFields(string $pageName): iterable + { + [ + 'linkTypeField' => $linkTypeField, + 'nameField' => $nameField, + 'linkField' => $linkField, + 'socialMediaTypeField' => $socialMediaTypeField, + 'parentField' => $parentField, + 'positionHumanField' => $positionHumanField, + ] = $this->getFields($pageName); + + if ($pageName === Crud::PAGE_EDIT) { + /** @var ChoiceField $socialMediaTypeField */ + $socialMediaTypeField->setChoices(SocialMediaType::cases()); + + /** @var MenuItem $item */ + $item = $this->getContext()?->getEntity()->getInstance(); + + if ($item->getLinkType() === LinkType::LINK) { + return [$nameField, $linkField, $parentField]; + } + + // social media + return [$socialMediaTypeField, $linkField]; + } + + // show + list + + return [$nameField, $linkTypeField, $linkField, $parentField, $socialMediaTypeField, $positionHumanField]; + } +} diff --git a/src/Controller/Admin/AbstractProductCrudController.php b/src/Controller/Admin/AbstractProductCrudController.php new file mode 100755 index 0000000..8db4bf8 --- /dev/null +++ b/src/Controller/Admin/AbstractProductCrudController.php @@ -0,0 +1,276 @@ +setEntityLabelInPlural($this->getEntityLabelInPlural()) + ->setEntityLabelInSingular($this->getEntityLabelInSingular()) + ->setSearchFields(['name', 'description']) + ->setDefaultSort(['createdAt' => 'DESC']) + ->setFormThemes([ + '@EasyAdmin/crud/form_theme.html.twig', + 'easy_admin/crud/form_theme.html.twig', + ]) + ; + } + + public function configureFilters(Filters $filters): Filters + { + return $filters + ->add(UuidFilter::new('id')) + ->add(EnumFilter::new('status', ProductStatusType::class)) + ->add(EnumFilter::new('visibility', ProductVisibilityType::class)) + ->add('category') + ->add('owner') + ->add('name') + ->add('description') + ; + } + + public function configureActions(Actions $actions): Actions + { + $onBreak = Action::new('onBreak', 'action.onBreak') + ->linkToCrudAction('changeStatus') + ->displayIf(static function (Product $product) { + return $product->isActive(); + }); + + $activate = Action::new('activate', 'action.activate') + ->linkToCrudAction('changeStatus') + ->displayIf(static function (Product $product) { + return $product->isPaused(); + }); + + $availability = Action::new('availability', 'action.availability') + ->linkToCrudAction('linkToProductAvailabilityPage'); + + return $actions + ->add(Crud::PAGE_INDEX, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::INDEX) + ->add(Crud::PAGE_INDEX, $onBreak) + ->add(Crud::PAGE_INDEX, $activate) + ->add(Crud::PAGE_DETAIL, $availability); + } + + private function redirectToObjectCrudPage(): RedirectResponse + { + $this->adminUrlGenerator->setController(ObjectCrudController::class)->setAction('index')->removeReferrer()->setEntityId(null); + + return $this->redirect($this->adminUrlGenerator->generateUrl()); + } + + public function linkToProductAvailabilityPage(): Response + { + return $this->render('/admin/product/availability_product.html.twig'); + } + + public function changeStatus(AdminContext $context): Response + { + /** @var Product $product */ + $product = $context->getEntity()->getInstance(); + if ($product->isPaused()) { + $product->setStatus(ProductStatus::ACTIVE); + } else { + $product->setStatus(ProductStatus::PAUSED); + } + $this->productRepository->save($product, true); + + return $this->redirectToObjectCrudPage(); + } + + public static function getEntityFqcn(): string + { + return Product::class; + } + + public function createEntity(string $entityFqcn): Product + { + /** @var Product $product */ + $product = new $entityFqcn(); + $product->setType($this->getProductType()); + + return $product; + } + + /** + * Only display a given product type. + */ + public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder + { + $qb = parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters); + $alias = $qb->getRootAliases()[0] ?? null; + $qb->andWhere($alias.'.type = :type') + ->setParameter('type', $this->getProductType()); + + return $qb; + } + + /** + * Return all possible product fields. + * + * @return array + */ + public function getFields(string $pageName): array + { + $idField = IdField::new('id') + ->setLabel('id') + ->hideOnForm(); + + $typeField = ChoiceField::new('type') + ->setFormType(EnumType::class) + ->setFormTypeOption('class', ProductType::class) + ->setChoices(ProductType::getAsArray()); + + $statusField = ChoiceField::new('status') + ->setFormType(EnumType::class) + ->setFormTypeOption('class', ProductStatus::class) + ->setChoices(ProductStatus::getAsArray()); + + $visibilityField = ChoiceField::new('visibility') + ->setFormType(EnumType::class) + ->setFormTypeOption('class', ProductVisibility::class) + ->setChoices(ProductVisibility::getAsArray()); + $groupsField = CollectionField::new('groups'); + + $ownerField = AssociationField::new('owner'); + $categoryField = AssociationField::new('category') + ->setQueryBuilder(function (QueryBuilder $queryBuilder) { + return $this->categoryRepository->addTypeFilter($queryBuilder, $this->getProductType()); + }) + ; + + $nameField = TextField::new('name'); + $descriptionField = TextareaField::new('description'); + + // objects + $ageField = TextField::new('age'); + $depositField = MoneyField::new('deposit') + ->setCurrencyPropertyPath('currency') + ->setStoredAsCents() + ->setNumDecimals(0) + ; + $currencyField = CurrencyField::new('currency'); + + // services + $durationField = TextField::new('duration'); + + $createdAt = DateTimeField::new('createdAt'); + $updatedAt = DateTimeField::new('updatedAt'); + + $imageField = ImageField::new('images') + ->setBasePath($this->productBasePath) // correctly set the formatted value available in the template + ->setUploadDir('public'.$this->productBasePath) + ->setUploadedFileNamePattern('[uuid].[extension]') + ->setFormTypeOption('upload_new', $this->easyAdminHelper->getUploadNewCallback($this->productStorage)) + ->setFormTypeOption('upload_delete', $this->easyAdminHelper->getUploadDeleteCallback($this->productStorage)) + ->setFormTypeOption('required', false) + ->setFormTypeOption('multiple', true) + ->setFormTypeOption('constraints', $this->mediaManager->getImageArrayConstraints()) + ->setTemplatePath('easy_admin/field/flysystem_images.html.twig') + ->setCustomOption('first_image_only', true) + ->setHelp($this->mediaManager->getHelpMessage()) + ->setSortable(false) + ; + + $addressField = TextField::new('address'); + + if ($pageName === Crud::PAGE_DETAIL) { + /** @var Product $product */ + $product = $this->getContext()?->getEntity()->getInstance(); + + $addressField = TextField::new('address')->setValue($product->getOwner()->getAddress()?->getDisplayName()); + } + + $preferredLoanDuration = TextField::new('preferredLoanDuration'); + + return compact( + 'idField', + 'typeField', + 'statusField', + 'visibilityField', + 'groupsField', + 'ownerField', + 'categoryField', + 'nameField', + 'descriptionField', + 'ageField', + 'durationField', + 'depositField', + 'currencyField', + 'createdAt', + 'updatedAt', + 'imageField', + 'addressField', + 'preferredLoanDuration', + ); + } +} diff --git a/src/Controller/Admin/AbstractUserCrudController.php b/src/Controller/Admin/AbstractUserCrudController.php new file mode 100755 index 0000000..c914d39 --- /dev/null +++ b/src/Controller/Admin/AbstractUserCrudController.php @@ -0,0 +1,446 @@ +mailer = $mailer; + } + + public function configureCrud(Crud $crud): Crud + { + return $crud + ->setEntityLabelInPlural($this->getEntityLabelInPlural()) + ->setEntityLabelInSingular($this->getEntityLabelInSingular()) + ->setSearchFields(['email', 'firstname', 'lastname', 'name']) + ->setDefaultSort(['id' => 'ASC']) + ->setFormThemes([ + '@EasyAdmin/crud/form_theme.html.twig', + 'easy_admin/crud/form_theme.html.twig', + ]) + ; + } + + public function configureFilters(Filters $filters): Filters + { + return $filters + ->add(UuidFilter::new('id')) + ->add(GroupFilter::new('group')) + ->add('email') + ->add('firstname') + ->add('lastname') + ->add('enabled') + ->add('createdAt') + ->add('updatedAt') + ->add(DateTimeFilter::new('loginAt') + ->setFormTypeOption('value_type_options', ['widget' => 'choice']) + ) + ; + } + + public function configureActions(Actions $actions): Actions + { + $currentUser = $this->security->getUser(); + + $connectAs = Action::new('connectAs', 'action.connectAs', 'fas fa-sign-in-alt') + ->linkToCrudAction('connectAs') + ->displayIf(fn (User $user) => $currentUser !== $user && $user->isEnabled() && $user->isEmailConfirmed()); + $actions + ->add(Crud::PAGE_INDEX, $connectAs); + + $promoteToAdmin = Action::new('promoteToAdmin', 'action.promoteToAdmin') + ->linkToCrudAction('promoteToAdmin') + ->displayIf(fn (User $user) => !$user->isAdmin() && !$user->isMainAdminAccount()); + $actions + ->add(Crud::PAGE_INDEX, $promoteToAdmin); + + $deleteCallback = function (Action $action) use ($currentUser) { + return $action->displayIf(fn (User $user) => $currentUser !== $user && !$user->isMainAdminAccount()); + }; + $actions->update(Crud::PAGE_INDEX, 'delete', $deleteCallback); + $actions->update(Crud::PAGE_DETAIL, 'delete', $deleteCallback); + + $exportAction = Action::new('export') + ->linkToUrl(function () { + /** @var AdminContext $context */ + $context = $this->getContext(); + + return $this->adminUrlGenerator->setAll($context->getRequest()->query->all()) + ->setEntityId(null) + ->setAction('export') + ->generateUrl(); + }) + ->addCssClass('btn btn-success') + ->setIcon('fa fa-download') + ->createAsGlobalAction() + ; + $actions->add(Crud::PAGE_INDEX, $exportAction); + + $viewPayments = Action::new('payments') + ->linkToUrl(function () { + /** @var AdminContext $context */ + $context = $this->getContext(); + /** @var User $user */ + $user = $context->getEntity()->getInstance(); + + return $this->adminUrlGenerator + ->unsetAll() + ->setController(PaymentCrudController::class) + ->setAction(Action::INDEX) + ->set('filters[user]', $user->getId()) + ->generateUrl(); + }) + ->displayIf(fn (User $user) => !$user->getPayments()->isEmpty()) + ->setIcon('fas fa-credit-card') + ; + $actions->add(Crud::PAGE_DETAIL, $viewPayments); + + return $actions + ->add(Crud::PAGE_EDIT, Action::DETAIL) + ->add(Crud::PAGE_INDEX, Action::DETAIL) + ; + } + + /** + * Impersonate action so we can do some more processing before changing user. + */ + public function connectAs(AdminContext $context): Response + { + /** @var User $targetUser */ + $targetUser = $context->getEntity()->getInstance(); + + $message = new TranslatableMessage( + 'flash.warning.connectAs', + ['%target_user%' => $targetUser->getUserIdentifier()], + DashboardController::DOMAIN + ); + + $this->addFlash( + 'warning', + $message + ); + $route = $targetUser->isAdmin() ? 'admin' : 'home'; // if user is not admin redirect to home page + + return $this->redirectToRoute($route, [self::SWITCH_USER_PARAMETER => $targetUser->getUserIdentifier()]); + } + + /** + * @throws TransportExceptionInterface + */ + public function promoteToAdmin(AdminContext $context): Response + { + /** @var User $targetUser */ + $targetUser = $context->getEntity()->getInstance(); + $targetUser->setType(UserType::ADMIN)->promoteToAdmin(); + $this->userManager->save($targetUser, true); + + $message = new TranslatableMessage( + 'flash.success.promoteToAdmin', + ['%target_user%' => $targetUser->getUserIdentifier()], + DashboardController::DOMAIN + ); + + $this->addFlash( + 'success', + $message + ); + + $userContext = []; + $userContext['user'] = $targetUser; + $this->mailer->send(PromoteToAdminEmail::class, $userContext); + + return $this->redirect( + $this + ->adminUrlGenerator + ->unsetAll() + ->setController(AdministratorCrudController::class) + ->setAction(Action::INDEX) + ->generateUrl() + ); + } + + public static function getEntityFqcn(): string + { + return User::class; + } + + /** + * We consider the admin know what he does, in case of error he can simply modify + * the email. + */ + public function createEntity(string $entityFqcn): User + { + /** @var User $user */ + $user = new $entityFqcn(); + $user->setType($this->getUserType()); + $this->userManager->finalizeAccountCreateStep2($user); + + return $user; + } + + /** + * Only display a given user type. + */ + public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder + { + $qb = parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters); + $alias = $qb->getRootAliases()[0] ?? null; + $qb->andWhere($alias.'.type = :type') + ->setParameter('type', $this->getUserType()); + + return $qb; + } + + /** + * Return all possible user fields. + * + * @return array + */ + public function getFields(string $pageName): array + { + $i18prefix = $this->getI18nPrefix(self::class); + $idField = IdField::new('id') + ->setLabel('id') + ->hideOnForm(); + + $emailField = EmailField::new('email'); + $firstNameField = TextField::new('firstname')->setRequired(true); + $lastNameField = TextField::new('lastname')->setRequired(true); + $nameField = TextField::new('name')->setRequired(true); + + $plainPassword = TextField::new('plainPassword') + ->setFormType(RepeatedType::class) + ->setFormTypeOptions([ + 'type' => PasswordType::class, + 'required' => $pageName === Crud::PAGE_NEW, + 'first_options' => [ + 'attr' => [ + 'autocomplete' => 'new-password', + ], + ], + 'second_options' => [ + 'attr' => [ + 'autocomplete' => 'new-password', + ], + ], + ]); + + $enabledField = $this->getSimpleBooleanField('enabled'); + $emailConfirmedField = $this->getSimpleBooleanField('emailConfirmed'); + + $typeField = ChoiceField::new('type') + ->setFormType(EnumType::class) + ->setFormTypeOption('class', UserType::class) + ->setChoices(UserType::getAsArray()); + + $loginAt = DateTimeField::new('loginAt'); + $createdAt = DateTimeField::new('createdAt'); + $updatedAt = DateTimeField::new('updatedAt'); + + $avatarField = ImageField::new('avatar') + ->setBasePath($this->userBasePath) // correctly set the formatted value available in the template + ->setUploadDir('public'.$this->userBasePath) + ->setUploadedFileNamePattern('[uuid].[extension]') + ->setFormTypeOption('upload_new', $this->easyAdminHelper->getUploadNewCallback($this->userStorage)) + ->setFormTypeOption('upload_delete', $this->easyAdminHelper->getUploadDeleteCallback($this->userStorage)) + ->setFormTypeOption('constraints', $this->mediaManager->getFileConstraints()) + ->setTemplatePath('easy_admin/field/flysystem_image.html.twig') + ->setHelp($this->mediaManager->getHelpMessage()) + ; + + $phoneNumberField = TextField::new('phone') + ->setFormType(PhoneNumberType::class) + ->setFormTypeOptions([ + 'format' => PhoneNumberFormat::INTERNATIONAL, + 'required' => false, + ]) + ->setHelp($i18prefix.'.field.phone.help') + ; + + $scheduleField = TextField::new('schedule'); + $categoryField = AssociationField::new('category') + ->setFormTypeOption('choice_label', function (Category $category) { + return $this->translator->trans($category->getType()->name, [], 'admin').' / '.$category->getName(); + }) + ->setRequired(false) + ; + $descriptionField = TextareaField::new('description'); + $smsNotificationsField = BooleanField::new('smsNotifications'); + $vacationModeField = BooleanField::new('vacationMode'); + $addressField = AssociationField::new('address'); + + return compact( + 'idField', + 'emailField', + 'firstNameField', + 'lastNameField', + 'nameField', + 'plainPassword', + 'enabledField', + 'emailConfirmedField', + 'typeField', + 'loginAt', + 'createdAt', + 'updatedAt', + 'avatarField', + 'phoneNumberField', + 'scheduleField', + 'categoryField', + 'descriptionField', + 'smsNotificationsField', + 'vacationModeField', + 'addressField', + ); + } + + /** + * For now, we export exactly what we see in the list to avoid security problems. + */ + public function export(AdminContext $context): Response + { + $fields = FieldCollection::new($this->configureFields(Crud::PAGE_INDEX)); + /** @var CrudDto $crud Crud is defined here */ + $crud = $context->getCrud(); + $filters = $this->filterFactory->create($crud->getFiltersConfig(), $fields, $context->getEntity()); + /** @var SearchDto $search */ + $search = $context->getSearch(); + $queryBuilder = $this->createIndexQueryBuilder($search, $context->getEntity(), $fields, $filters); + $fileName = $this->slugger->slug($this->translator->trans($this->getEntityLabelInPlural(), [], DashboardController::DOMAIN)); + + return $this->csvExporter->createResponseFromQueryBuilder($queryBuilder, $fields, $fileName.'.csv'); + } + + /** + * We can't delete the main admin account or ourself. This is an additional + * protection as the delete button is already disabled in the list. + */ + public function delete(AdminContext $context) + { + /** @var User $userToDelete */ + $userToDelete = $context->getEntity()->getInstance(); + $currentUser = $this->security->getUser(); + if ($userToDelete === $currentUser || $userToDelete->isMainAdminAccount()) { + throw $this->createAccessDeniedException('Cannot delete this user (self or main admin account).'); + } + + return parent::delete($context); + } + + /** + * Special process for the phone number as it uses a custom form type. + * + * @see EditProfileFormType::onPostSubmit + */ + public function updateEntity(EntityManagerInterface $entityManager, mixed $entityInstance): void + { + /** @var User $entityInstance */ + $entityInstance->changePhoneNumber($entityInstance->phone); + + parent::updateEntity($entityManager, $entityInstance); + } + + /** + * We need to normalize the email to make work the unique entity properly. + */ + public function createNewFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface + { + $builder = $this->container->get(FormFactory::class)->createNewFormBuilder($entityDto, $formOptions, $context); + $this->userManager->addEmailNormalizeSubmitEvent($builder); + + return $builder; + } + + public function createEditFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface + { + $builder = $this->container->get(FormFactory::class)->createEditFormBuilder($entityDto, $formOptions, $context); + $this->userManager->addEmailNormalizeSubmitEvent($builder); + + return $builder; + } +} diff --git a/src/Controller/Admin/AdminSecuredCrudControllerInterface.php b/src/Controller/Admin/AdminSecuredCrudControllerInterface.php new file mode 100644 index 0000000..a8929ad --- /dev/null +++ b/src/Controller/Admin/AdminSecuredCrudControllerInterface.php @@ -0,0 +1,16 @@ +promoteToAdmin(); + + return $user; + } + + /** + * @throws TransportExceptionInterface + */ + public function persistEntity(EntityManagerInterface $entityManager, mixed $entityInstance): void + { + parent::persistEntity($entityManager, $entityInstance); + + $context = []; + $context['user'] = $entityInstance; + $this->mailer->send(NewAdminEmail::class, $context); + } + + public function configureFields(string $pageName): iterable + { + $panels = $this->getPanels(); + + [ + 'idField' => $idField, + 'emailField' => $emailField, + 'firstNameField' => $firstNameField, + 'lastNameField' => $lastNameField, + 'plainPassword' => $plainPassword, + 'enabledField' => $enabledField, + 'loginAt' => $loginAt, + 'createdAt' => $createdAt, + 'updatedAt' => $updatedAt, + ] = $this->getFields($pageName); + + if ($pageName === Crud::PAGE_INDEX) { + return [ + $emailField, + $firstNameField, + $lastNameField, + $enabledField, + $createdAt, + $updatedAt, + $loginAt, + ]; + } + + if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { + return [ + $emailField, + $firstNameField, + $lastNameField, + $plainPassword, + $enabledField, + ]; + } + + // show + return [ + $panels['information'], + $emailField, + $firstNameField, + $lastNameField, + $enabledField, + + $panels['tech_information'], + $idField, + $loginAt, + $createdAt, + $updatedAt, + ]; + } +} diff --git a/src/Controller/Admin/CategoryObjectCrudController.php b/src/Controller/Admin/CategoryObjectCrudController.php new file mode 100644 index 0000000..87d0992 --- /dev/null +++ b/src/Controller/Admin/CategoryObjectCrudController.php @@ -0,0 +1,25 @@ + 1, + + // content + MenuItemCrudController::class => 4, + MenuItemFooterCrudController::class => 5, + CategoryObjectCrudController::class => 6, // +1 + + // usage + UserCrudController::class => 8, + PlaceCrudController::class => 9, + + // Group (+1) + ObjectCrudController::class => 11, + ]; + + public function __construct( + private readonly TranslatorInterface $translator, + private readonly GroupRepository $groupRepository, + private readonly AdminUrlGenerator $adminUrlGenerator, + private readonly AuthorizationChecker $authorizationChecker, + ) { + } + + /** + * @see https://symfony.com/bundles/EasyAdminBundle/current/dashboards.html#/translation + */ + public function configureDashboard(): Dashboard + { + return Dashboard::new() + ->setTitle($this->translator->trans('dashboard.title', [], self::DOMAIN)) + ->setTranslationDomain('admin') + ->generateRelativeUrls() + ->setLocales([ + 'en' => '🇬🇧 Anglais', + 'fr' => '🇫🇷 Français', + ]) + ; + } + + #[Route('/admin', name: 'admin')] + public function index(): Response + { + if (!$this->authorizationChecker->isAdmin()) { + $groupUrl = $this->adminUrlGenerator + ->setController(GroupCrudController::class) + ->generateUrl(); + + return $this->redirect($groupUrl); + } + + return $this->render('admin/dashboard.html.twig', [ + 'group_count' => $this->groupRepository->count([]), + ]); + } + + public function configureMenuItems(): iterable + { + /** @var User $user */ + $user = $this->getUser(); + + // ————————————————————————————————————————————————————————————————————— + yield MenuItem::linkToDashboard('menu.dashboard', 'fa fa-home')->setPermission(User::ROLE_ADMIN); + + $url = $this->adminUrlGenerator + ->unsetAll() + ->setController(AdministratorCrudController::class) + ->set('crudAction', Crud::PAGE_INDEX) + ->set('menuIndex', self::MENU_INDEX[AdministratorCrudController::class]); + + yield MenuItem::linkToUrl('menu.administrators', 'fas fa-user-plus', $url->generateUrl())->setPermission(User::ROLE_ADMIN); + yield MenuItem::linkToRoute('menu.parameters', 'fas fa-cog', ParametersController::ROUTE_NAME)->setPermission(User::ROLE_ADMIN)->setPermission(User::ROLE_ADMIN); + + // ————————————————————————————————————————————————————————————————————— + yield MenuItem::section('menu.content'); + + $menuConfigUrl = $this->adminUrlGenerator + ->unsetAll() + ->setController(MenuItemCrudController::class) + ->set('crudAction', Crud::PAGE_INDEX) + ->set('menuIndex', self::MENU_INDEX[MenuItemCrudController::class]) + ->generateUrl(); + $footerConfigUrl = $this->adminUrlGenerator + ->unsetAll() + ->setController(MenuItemFooterCrudController::class) + ->set('crudAction', Crud::PAGE_INDEX) + ->set('menuIndex', self::MENU_INDEX[MenuItemFooterCrudController::class]) + ->generateUrl(); + + $categoryObjectUrl = $this->adminUrlGenerator + ->unsetAll() + ->setController(CategoryObjectCrudController::class) + ->set('crudAction', Crud::PAGE_INDEX) + ->set('menuIndex', self::MENU_INDEX[CategoryObjectCrudController::class]) + ->set('submenuIndex', 0) + ->generateUrl(); + $categoryServiceUrl = $this->adminUrlGenerator + ->unsetAll() + ->setController(CategoryServiceCrudController::class) + ->set('crudAction', Crud::PAGE_INDEX) + ->set('menuIndex', self::MENU_INDEX[CategoryObjectCrudController::class]) + ->set('submenuIndex', 1) + ->generateUrl(); + + yield MenuItem::linkToUrl('menu.config_menu', 'fa-solid fa-bars', $menuConfigUrl)->setPermission(User::ROLE_ADMIN); + yield MenuItem::linkToUrl('menu.config_footer', 'fas fa-ellipsis-h', $footerConfigUrl)->setPermission(User::ROLE_ADMIN); + + yield MenuItem::linkToCrud('menu.pages', 'fas fa-hat-wizard', Page::class)->setPermission(User::ROLE_ADMIN); + + yield MenuItem::subMenu('menu.categories', 'fa-solid fa-folder')->setSubItems([ + MenuItem::linkToUrl('menu.objects', 'fa-solid fa-box', $categoryObjectUrl)->setPermission(User::ROLE_ADMIN), + MenuItem::linkToUrl('menu.services', 'fa-regular fa-handshake', $categoryServiceUrl)->setPermission(User::ROLE_ADMIN), + ])->setPermission(User::ROLE_ADMIN); + + // ————————————————————————————————————————————————————————————————————— + yield MenuItem::section('menu.usage')->setPermission(User::ROLE_ADMIN); + + $url = $this->adminUrlGenerator + ->unsetAll() + ->setController(UserCrudController::class) + ->set('crudAction', Crud::PAGE_INDEX) + ->set('menuIndex', self::MENU_INDEX[UserCrudController::class]) + ; + yield MenuItem::linkToUrl('menu.users', 'fas fa-user', $url->generateUrl())->setPermission(User::ROLE_ADMIN); + $url = $this->adminUrlGenerator + ->unsetAll() + ->setController(PlaceCrudController::class) + ->set('crudAction', Crud::PAGE_INDEX) + ->set('menuIndex', self::MENU_INDEX[PlaceCrudController::class]) + ; + + yield MenuItem::linkToUrl('menu.places', 'fas fa-location-dot', $url->generateUrl())->setPermission(User::ROLE_ADMIN); + + yield MenuItem::subMenu('menu.groups', 'fas fa-users')->setSubItems([ + MenuItem::linkToCrud('menu.groups', 'fas fa-users', Group::class)->setPermission(User::ROLE_GROUP_ADMIN), + MenuItem::linkToCrud('menu.members', 'fas fa-user-friends', UserGroup::class)->setPermission(User::ROLE_GROUP_ADMIN), + ])->setPermission(User::ROLE_GROUP_ADMIN); + + $objectUrl = $this->adminUrlGenerator + ->unsetAll() + ->setController(ObjectCrudController::class) + ->set('crudAction', Crud::PAGE_INDEX) + ->set('menuIndex', self::MENU_INDEX[ObjectCrudController::class]) + ->set('submenuIndex', 0) + ->generateUrl(); + + $serviceUrl = $this->adminUrlGenerator + ->unsetAll() + ->setController(ServiceCrudController::class) + ->set('crudAction', Crud::PAGE_INDEX) + ->set('menuIndex', self::MENU_INDEX[ObjectCrudController::class]) + ->set('submenuIndex', 1) + ->generateUrl(); + + yield MenuItem::subMenu('menu.articles', 'fa-solid fa-box')->setSubItems([ + MenuItem::linkToUrl('menu.objects', 'fa-solid fa-box', $objectUrl)->setPermission(User::ROLE_ADMIN), + MenuItem::linkToUrl('menu.services', 'fa-regular fa-handshake', $serviceUrl)->setPermission(User::ROLE_ADMIN), + ])->setPermission(User::ROLE_ADMIN); + + yield MenuItem::linkToCrud('menu.loans', 'fas fa-link', ServiceRequest::class)->setPermission(User::ROLE_ADMIN); + + // ————————————————————————————————————————————————————————————————————— + if ($user->isDevAccount()) { + yield MenuItem::section('menu.devtools')->setPermission(User::ROLE_ADMIN); + yield MenuItem::linkToRoute('menu.dev_tools', 'fas fa-wrench', DevToolsController::ROUTE_NAME)->setPermission(User::ROLE_ADMIN); + } + + // ————————————————————————————————————————————————————————————————————— + yield MenuItem::section('menu.public'); + yield MenuItem::linkToUrl('menu.home', 'fa fa-home', '/')->setLinkTarget('_blank'); + yield MenuItem::linkToUrl('menu.user', 'fa fa-user', $this->generateUrl(MyAccountAction::ROUTE))->setLinkTarget('_blank'); + } +} diff --git a/src/Controller/Admin/Dev/DevToolsController.php b/src/Controller/Admin/Dev/DevToolsController.php new file mode 100755 index 0000000..a56dc93 --- /dev/null +++ b/src/Controller/Admin/Dev/DevToolsController.php @@ -0,0 +1,44 @@ +render('admin/dev/dev_tools.html.twig', compact( + 'transCodes', + 'uuidV4', + 'uuidV6', + 'encoded', + 'urlEncoded', + 'confirmationCode1', + 'confirmationCode2', + )); + } +} diff --git a/src/Controller/Admin/FooterCrudController.php b/src/Controller/Admin/FooterCrudController.php new file mode 100644 index 0000000..b98c750 --- /dev/null +++ b/src/Controller/Admin/FooterCrudController.php @@ -0,0 +1,33 @@ +setEntityLabelInPlural('groups') + ->setSearchFields(['name', 'description']) + ->setDefaultSort(['id' => 'ASC']) + ; + } + + public function configureFilters(Filters $filters): Filters + { + return $filters + ->add(UuidFilter::new('id')) + ->add(EnumFilter::new('type', GroupTypeType::class)) + ->add(EnumFilter::new('membership', GroupMembershipType::class)) + ->add('name') + ->add('description') + ; + } + + public function configureActions(Actions $actions): Actions + { + $exportAction = Action::new('export') + ->linkToUrl(function () { + /** @var AdminContext $context */ + $context = $this->getContext(); + + return $this->adminUrlGenerator->setAll($context->getRequest()->query->all()) + ->setEntityId(null) + ->setAction('export') + ->generateUrl(); + }) + ->addCssClass('btn btn-success') + ->setIcon('fa fa-download') + ->createAsGlobalAction() + ; + + $listMembers = Action::new('listMembers') + ->linkToUrl(function () { + /** @var Group $group */ + $group = $this->getContext()?->getEntity()->getInstance(); + + return $this->adminUrlGenerator + ->unsetAll() + ->setController(UserGroupCrudController::class) + ->set('filters[group]', $group->getId()) + ->setAction('index') + ->generateUrl(); + }) + ->setIcon('fas fas-user-friends') + ; + $offersList = Action::new('offersList', 'offers_list') + ->linkToUrl(function () { + /** @var Group $group */ + $group = $this->getContext()?->getEntity()->getInstance(); + + return $this->adminUrlGenerator + ->unsetAll() + ->setController(GroupOfferCrudController::class) + ->set('filters[group]', $group->getId()) + ->setAction('index') + ->generateUrl(); + }) + ->displayIf(function () { + /** @var Group $group */ + $group = $this->getContext()?->getEntity()->getInstance(); + + return $group->getMembership()->isCharged(); + }); + + $offersListIndexPage = Action::new('offersList', 'offers_list') + ->linkToCrudAction('redirectToOffersList') + ->displayIf(static function (Group $group) { + return $group->getMembership()->isCharged(); + }); + + $actions + ->add(Crud::PAGE_INDEX, $exportAction) + ->add(Crud::PAGE_EDIT, $listMembers) + ->add(Crud::PAGE_DETAIL, $listMembers) + ->add(Crud::PAGE_DETAIL, $offersList) + ->add(Crud::PAGE_INDEX, $offersListIndexPage); + + /** @var User $user */ + $user = $this->getUser(); + + // display the invite link if we are an admin, the main group admin or the parameter is activated + $inviteAction = Action::new('invite', 'invite', 'fa fa-user-plus') + ->linkToCrudAction('invite') + ->displayIf(fn (Group $group) => $this->authorizationChecker->isAdmin() || $group->isMainAdmin($user) || $group->isInvitationByAdmin()) + ; + $actions + ->add(Crud::PAGE_INDEX, $inviteAction); + + // group admin can't create a group from here but he can edit its groups + if (!$this->authorizationChecker->isAdmin()) { + $actions + ->remove(Crud::PAGE_INDEX, Action::NEW) + ->remove(Crud::PAGE_INDEX, Action::DELETE) + ->remove(Crud::PAGE_DETAIL, Action::DELETE) + ; + } + + return $actions + ->add(Crud::PAGE_INDEX, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::INDEX) + ; + } + + public function redirectToOffersList(AdminContext $context): Response + { + $group = $context->getEntity()->getInstance(); + $this->adminUrlGenerator + ->unsetAll() + ->setController(GroupOfferCrudController::class) + ->set('filters[group]', $group->getId()) + ->setAction('index'); + + return $this->redirect($this->adminUrlGenerator->generateUrl()); + } + + public static function getEntityFqcn(): string + { + return Group::class; + } + + /** + * When a group admin is logged, we must restrict the groups he can access to. + */ + public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder + { + // admins can see everything + $qb = parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters); + if ($this->authorizationChecker->isAdmin()) { + return $qb; + } + + /** @var User $user */ + $user = $this->getUser(); + $qb->andWhere(sprintf('%s.id IN (:groups)', $qb->getRootAliases()[0] ?? '')) + ->setParameter(':groups', $user->getMyGroupsAsAdmin()); + + return $qb; + } + + public function configureFields(string $pageName): iterable + { + $idFIeld = IdField::new('id') + ->setLabel('id') + ->hideOnForm(); + + $typeField = ChoiceField::new('type') + ->setFormType(EnumType::class) + ->setFormTypeOption('class', GroupType::class) + ->setChoices(GroupType::getAsArray()); + $membershipField = ChoiceField::new('membership') + ->setFormType(EnumType::class) + ->setFormTypeOption('class', GroupMembership::class) + ->setChoices(GroupMembership::getAsArray()); + + $parentField = AssociationField::new('parent') + ->setRequired(false); + $childrenField = AssociationField::new('children'); + $usersField = AssociationField::new('userGroups') + ->setTemplatePath('admin/group/user_groups_field.html.twig'); + + $nameField = TextField::new('name'); + $descriptionField = TextareaField::new('description'); + + $url = UrlField::new('url'); + $createdAt = DateTimeField::new('createdAt'); + $updatedAt = DateTimeField::new('updatedAt'); + + $invitationByAdminField = BooleanField::new('invitationByAdmin')->renderAsSwitch(false); + $panels = $this->getPanels(); + + if ($pageName === Crud::PAGE_INDEX) { + return [$nameField, $typeField, $parentField, $membershipField, $usersField, $createdAt, $updatedAt]; + } + + if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { + $typeField->setChoices(GroupType::cases()); + $membershipField->setChoices(GroupMembership::cases()); + + return [ + $nameField, + $typeField, + $membershipField, + $parentField, + $descriptionField, + $url, + $invitationByAdminField, + $membershipField, + ]; + } + + // show + + return [ + $panels['information'], + $nameField, + $parentField, + $childrenField, + $typeField, + $membershipField, + $descriptionField, + $invitationByAdminField, + + $panels['tech_information'], + $idFIeld, + $updatedAt, + $createdAt, + ]; + } + + /** + * Custom action that allows sending invitations to users. We only need the email + * here as the rest of the process is handled by the step 2 form of the account + * creation workflow. + * + * @see GroupCrudControllerTest::testInviteActionSuccess() + */ + public function invite(Request $request): Response + { + /** @var Group $group */ + $group = $this->getContext()?->getEntity()->getInstance(); + $user = new User(); + $form = $this->createForm(GroupInvitationFormType::class, $user)->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $this->commandBus->dispatch(new CreateGroupInvitationMessage($user->getEmail(), $group->getId())); + $this->addFlashSuccess($this->getI18nPrefix().'.invite.flash.success'); + + return $this->redirect($this->adminUrlGenerator->unsetAll()->setController(self::class)->generateUrl()); + } + + return $this->render('admin/group/invite.html.twig', compact('form', 'group')); + } + + /** + * For now we export exactly what we see in the list to avoid seurity problems. + */ + public function export(AdminContext $context): Response + { + $fields = FieldCollection::new($this->configureFields(Crud::PAGE_INDEX)); + /** @var CrudDto $crud Crud is defined here */ + $crud = $context->getCrud(); + + $filters = $this->filterFactory->create($crud->getFiltersConfig(), $fields, $context->getEntity()); + /** @var SearchDto $search */ + $search = $context->getSearch(); + $queryBuilder = $this->createIndexQueryBuilder($search, $context->getEntity(), $fields, $filters); + + $fileName = $this->slugger->slug($this->translator->trans('menu.groups', [], DashboardController::DOMAIN)); + + return $this->csvExporter->createResponseFromQueryBuilder($queryBuilder, $fields, $fileName.'.csv'); + } +} diff --git a/src/Controller/Admin/GroupOfferCrudController.php b/src/Controller/Admin/GroupOfferCrudController.php new file mode 100755 index 0000000..1597cca --- /dev/null +++ b/src/Controller/Admin/GroupOfferCrudController.php @@ -0,0 +1,180 @@ +setEntityLabelInPlural('group_offers') + ->setSearchFields(['name']) + ; + } + + public function configureFilters(Filters $filters): Filters + { + return $filters + ->add(UuidFilter::new('id')) + ->add(MyGroupFilter::new('group')) + ->add(EnumFilter::new('membership', GroupOfferTypeType::class)) + ->add('name') + ->add('active') + ; + } + + public function configureActions(Actions $actions): Actions + { + return $actions + ->add(Crud::PAGE_INDEX, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::INDEX) + ; + } + + public static function getEntityFqcn(): string + { + return GroupOffer::class; + } + + /** + * When a group admin is logged, we must restrict the groups he can access to. + */ + public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder + { + // admins can see everything + $qb = parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters); + if ($this->authorizationChecker->isAdmin()) { + return $qb; + } + + /** @var User $user */ + $user = $this->getUser(); + $qb->andWhere(sprintf('%s.group IN (:groups)', $qb->getRootAliases()[0] ?? '')) + ->setParameter(':groups', $user->getMyGroupsAsAdmin()); + + return $qb; + } + + public function configureFields(string $pageName): iterable + { + $idFIeld = IdField::new('id') + ->setLabel('id') + ->hideOnForm(); + $groupField = AssociationField::new('group') + ->setQueryBuilder(function (QueryBuilder $queryBuilder) { + /** @var User $user */ + $user = $this->getUser(); + + $qb = $queryBuilder->andWhere('entity.membership = :membership') + ->setParameter('membership', GroupMembership::CHARGED); + if (!$user->isAdmin()) { + $qb->join('entity.userGroups', 'ug') + ->andWhere('ug.membership = :userMembership') + ->andWhere('ug.user = :user') + ->setParameter('userMembership', UserMembership::ADMIN) + ->setParameter('user', $user); + } + }) + ->setRequired(false); + + $nameField = TextField::new('name'); + $typeField = ChoiceField::new('type') + ->setFormType(EnumType::class) + ->setFormTypeOption('class', GroupOfferType::class) + ->setChoices(GroupOfferType::getAsArray()); + + $priceField = MoneyField::new('price') + ->setCurrencyPropertyPath('currency') + ->setStoredAsCents(); + $currencyField = CurrencyField::new('currency'); + + $activeField = BooleanField::new('active') + ->setTemplatePath('easy_admin/field/boolean.html.twig') + ; + $createdAtField = DateTimeField::new('createdAt'); + $updatedAtField = DateTimeField::new('updatedAt'); + + $panels = $this->getPanels(); + if ($pageName === Crud::PAGE_INDEX) { + return [$groupField, $nameField, $typeField, $priceField, $activeField, $createdAtField, $updatedAtField]; + } + + if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { + $typeField->setChoices(GroupOfferType::cases()); + + return [ + $groupField, + $nameField, + $typeField, + $priceField, + $currencyField, + $activeField, + ]; + } + + // show + + return [ + $panels['information'], + $groupField, + $nameField, + $typeField, + $priceField, + $currencyField, + + $panels['tech_information'], + $idFIeld, + $updatedAtField, + $createdAtField, + ]; + } +} diff --git a/src/Controller/Admin/MenuCrudController.php b/src/Controller/Admin/MenuCrudController.php new file mode 100644 index 0000000..eb566d2 --- /dev/null +++ b/src/Controller/Admin/MenuCrudController.php @@ -0,0 +1,37 @@ + $nameField, + 'linkField' => $linkField, + 'parentField' => $parentField, + ] = $this->getFields($pageName); + + if ($pageName === Crud::PAGE_NEW) { + return [$nameField, $linkField, $parentField]; + } + + return parent::configureFields($pageName); + } +} diff --git a/src/Controller/Admin/MenuItemMenuSocialNetwordFooterCrudController.php b/src/Controller/Admin/MenuItemMenuSocialNetwordFooterCrudController.php new file mode 100644 index 0000000..da97d76 --- /dev/null +++ b/src/Controller/Admin/MenuItemMenuSocialNetwordFooterCrudController.php @@ -0,0 +1,39 @@ +setLinkType(LinkType::SOCIAL_NETWORK); + + return $menuItem; + } + + public function configureFields(string $pageName): iterable + { + [ + 'socialMediaTypeField' => $socialMediaTypeField, + 'linkField' => $linkField, + ] = $this->getFields($pageName); + + if ($pageName === Crud::PAGE_NEW) { + /** @var ChoiceField $socialMediaTypeField */ + $socialMediaTypeField->setChoices(SocialMediaType::cases()); + + return [$socialMediaTypeField, $linkField]; + } + + return parent::configureFields($pageName); + } +} diff --git a/src/Controller/Admin/MenuItemSocialNetworkCrudController.php b/src/Controller/Admin/MenuItemSocialNetworkCrudController.php new file mode 100644 index 0000000..bf008c9 --- /dev/null +++ b/src/Controller/Admin/MenuItemSocialNetworkCrudController.php @@ -0,0 +1,39 @@ +setLinkType(LinkType::SOCIAL_NETWORK); + + return $menuItem; + } + + public function configureFields(string $pageName): iterable + { + [ + 'socialMediaTypeField' => $socialMediaTypeField, + 'linkField' => $linkField, + ] = $this->getFields($pageName); + + if ($pageName === Crud::PAGE_NEW) { + /** @var ChoiceField $socialMediaTypeField */ + $socialMediaTypeField->setChoices(SocialMediaType::cases()); + + return [$socialMediaTypeField, $linkField]; + } + + return parent::configureFields($pageName); + } +} diff --git a/src/Controller/Admin/NewMenuFooterLinkController.php b/src/Controller/Admin/NewMenuFooterLinkController.php new file mode 100644 index 0000000..a47a25a --- /dev/null +++ b/src/Controller/Admin/NewMenuFooterLinkController.php @@ -0,0 +1,24 @@ + $nameField, + 'linkField' => $linkField, + ] = $this->getFields($pageName); + + if ($pageName === Crud::PAGE_NEW) { + return [$nameField, $linkField]; + } + + return parent::configureFields($pageName); + } +} diff --git a/src/Controller/Admin/ObjectCrudController.php b/src/Controller/Admin/ObjectCrudController.php new file mode 100644 index 0000000..560cabb --- /dev/null +++ b/src/Controller/Admin/ObjectCrudController.php @@ -0,0 +1,100 @@ +getPanels(); + + [ + 'idField' => $idField, + 'typeField' => $typeField, + 'statusField' => $statusField, + 'visibilityField' => $visibilityField, + 'groupsField' => $groupsField, + 'ownerField' => $ownerField, + 'categoryField' => $categoryField, + 'nameField' => $nameField, + 'descriptionField' => $descriptionField, + 'ageField' => $ageField, + 'depositField' => $depositField, + 'currencyField' => $currencyField, + 'imageField' => $imageField, + 'createdAt' => $createdAt, + 'updatedAt' => $updatedAt, + 'addressField' => $addressField, + 'preferredLoanDuration' => $preferredLoanDuration, + ] = $this->getFields($pageName); + + // list + if ($pageName === Crud::PAGE_INDEX) { + return [$nameField, $ownerField, $categoryField, $statusField, $visibilityField, $imageField, $createdAt]; + } + + /** @var ImageField $imageField */ + $imageField->setCustomOption('first_image_only', false); + + // forms + if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { + /** @var ChoiceField $statusField */ + $statusField->setChoices(ProductStatus::cases()); + /** @var ChoiceField $visibilityField */ + $visibilityField->setChoices(ProductVisibility::cases()); + + return [$nameField, $ownerField, $categoryField, $statusField, $visibilityField, $descriptionField, $ageField, $depositField, $currencyField, $imageField, $preferredLoanDuration]; + } + + // detail + + return [ + $panels['information'], + $ownerField, + $categoryField, + $statusField, + $visibilityField, + $groupsField, + $nameField, + $descriptionField, + $ageField, + $depositField, + $currencyField, + $imageField, + $addressField, + $preferredLoanDuration, + + $panels['tech_information'], + $idField, + $typeField, + $createdAt, + $updatedAt, ]; + } +} diff --git a/src/Controller/Admin/PageCrudController.php b/src/Controller/Admin/PageCrudController.php new file mode 100755 index 0000000..5129d86 --- /dev/null +++ b/src/Controller/Admin/PageCrudController.php @@ -0,0 +1,114 @@ +setEntityLabelInPlural('pages') + ->setSearchFields(['name']) + ->setDefaultSort(['name' => 'ASC']) + ->addFormTheme('@FOSCKEditor/Form/ckeditor_widget.html.twig'); + } + + public function __construct( + private readonly AdminUrlGenerator $adminUrlGenerator, + private readonly PageRepository $pageRepository, + ) { + } + + public function configureFilters(Filters $filters): Filters + { + return $filters + ->add('name') + ->add('enabled') + ->add('home') + ; + } + + public function configureActions(Actions $actions): Actions + { + $linkToFrontPage = Action::new('link', 'page.action.link') + ->linkToCrudAction('redirectToFrontPage') + ->displayAsLink(); + + return $actions + ->add(Crud::PAGE_INDEX, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::INDEX) + ->add(Crud::PAGE_DETAIL, $linkToFrontPage) + ; + } + + public function redirectToFrontPage(): RedirectResponse + { + /** @var Page $page */ + $page = $this->pageRepository->find($this->adminUrlGenerator->get('entityId')); + + return $this->redirectToRoute('app_cms_page', ['slug' => $page->getSlug()]); + } + + public static function getEntityFqcn(): string + { + return Page::class; + } + + public function configureFields(string $pageName): iterable + { + $idFIeld = IdField::new('id')->setLabel('id'); + $nameField = TextField::new('name'); + $contentField = TextEditorField::new('content')->setFormType(CKEditorType::class)->addCssFiles('ckeditor.css'); + $slugField = TextField::new('slug'); + $homeField = $this->getSimpleBooleanField('home'); + $enabledField = $this->getSimpleBooleanField('enabled'); + $createdAt = DateTimeField::new('createdAt'); + $updatedAt = DateTimeField::new('updatedAt'); + + if ($pageName === Crud::PAGE_INDEX) { + return [$nameField, $slugField, $homeField, $enabledField, $createdAt, $updatedAt]; + } + + if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { + return [$nameField, $homeField, $enabledField, $contentField]; + } + + // show + $panels = $this->getPanels(); + + return [ + $panels['information'], + $nameField, + $homeField, + $enabledField, + $contentField, + + $panels['tech_information'], + $idFIeld, + $slugField, + $createdAt, + $updatedAt, + ]; + } +} diff --git a/src/Controller/Admin/ParametersController.php b/src/Controller/Admin/ParametersController.php new file mode 100755 index 0000000..5bcfeda --- /dev/null +++ b/src/Controller/Admin/ParametersController.php @@ -0,0 +1,51 @@ +queryBus->query(new ParametersFormQuery()); + $form = $this->createForm(ParametersFormType::class, $parametersForm)->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + /** @var ParametersFormCommand $parametersForm */ + $parametersForm = $form->getData(); + $this->commandBus->dispatch($parametersForm); + $this->addFlash( + 'success', + 'parameters_controller.form.success', + ); + + return $this->redirect($this->adminUrlGenerator->setRoute(self::ROUTE_NAME)->generateUrl()); + } + + return $this->render('admin/parameters.html.twig', compact('form')); + } +} diff --git a/src/Controller/Admin/PaymentCrudController.php b/src/Controller/Admin/PaymentCrudController.php new file mode 100755 index 0000000..90105b8 --- /dev/null +++ b/src/Controller/Admin/PaymentCrudController.php @@ -0,0 +1,113 @@ +setEntityLabelInPlural('payments') + ->setDefaultSort(['createdAt' => 'DESC']); + } + + public function configureFilters(Filters $filters): Filters + { + $filters + ->add(UuidFilter::new('id')) + ->add(MyUsersFilter::new('user')) + ->add('totalAmount') + ; + + return $filters; + } + + public function configureActions(Actions $actions): Actions + { + return $actions + ->remove(Crud::PAGE_INDEX, Action::DELETE) + ->remove(Crud::PAGE_INDEX, Action::EDIT) + ->remove(Crud::PAGE_INDEX, Action::NEW) + ->remove(Crud::PAGE_DETAIL, Action::EDIT) + ->remove(Crud::PAGE_DETAIL, Action::DELETE) + ->add(Crud::PAGE_INDEX, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::INDEX) + ; + } + + public static function getEntityFqcn(): string + { + return Payment::class; + } + + public function configureFields(string $pageName): iterable + { + $idFIeld = IdField::new('id'); + $userField = AssociationField::new('user'); + $clientEmailField = TextField::new('clientEmail'); + $clientIdField = TextField::new('clientId'); + $numberFIeld = TextField::new('number'); + $descriptionField = TextField::new('description'); + $totalAmountField = MoneyField::new('totalAmount') + ->setCurrencyPropertyPath('currencyCode'); + $paidField = BooleanField::new('paid') + ->setTemplatePath('easy_admin/field/boolean.html.twig'); + $statusField = TextField::new('status'); + $methodField = TextField::new('method'); + $detailField = ArrayField::new('details') + ->setTemplatePath('easy_admin/field/json.html.twig'); + + $createdAt = DateTimeField::new('createdAt'); + $updatedAt = DateTimeField::new('updatedAt'); + + if ($pageName === Crud::PAGE_INDEX) { + return [$userField, $methodField, $totalAmountField, $paidField, $statusField, $descriptionField, $createdAt]; + } + + // show + $panels = $this->getPanels(); + + return [ + $panels['information'], + $userField, + $clientEmailField, + $methodField, + $totalAmountField, + $paidField, + $statusField, + $descriptionField, + + $panels['tech_information'], + $idFIeld, + $numberFIeld, + $clientIdField, + $detailField, + $createdAt, + $updatedAt, + ]; + } +} diff --git a/src/Controller/Admin/PlaceCrudController.php b/src/Controller/Admin/PlaceCrudController.php new file mode 100755 index 0000000..74be36f --- /dev/null +++ b/src/Controller/Admin/PlaceCrudController.php @@ -0,0 +1,62 @@ +getPanels(); + + [ + 'idField' => $idField, + 'emailField' => $emailField, + 'nameField' => $nameField, + 'phoneNumberField' => $phoneNumberField, + 'avatarField' => $avatarField, + 'descriptionField' => $descriptionField, + 'categoryField' => $categoryField, + 'smsNotificationsField' => $smsNotificationsField, + 'vacationModeField' => $vacationModeField, + 'scheduleField' => $scheduleField, + 'plainPassword' => $plainPassword, + 'enabledField' => $enabledField, + 'emailConfirmedField' => $emailConfirmedField, + 'loginAt' => $loginAt, + 'createdAt' => $createdAt, + 'updatedAt' => $updatedAt, + ] = $this->getFields($pageName); + + if ($pageName === Crud::PAGE_INDEX) { + return [$emailField, $nameField, $enabledField, $emailConfirmedField, $createdAt, $updatedAt, $loginAt]; + } + + if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { + return [$panels['information'], $emailField, $nameField, $phoneNumberField, $avatarField, $descriptionField, $categoryField, $smsNotificationsField, $vacationModeField, $scheduleField, $plainPassword, $panels['tech_information'], $enabledField, $emailConfirmedField]; + } + + // show + + return [$panels['information'], $emailField, $nameField, $phoneNumberField, $avatarField, $descriptionField, $scheduleField, $enabledField, $panels['tech_information'], $emailConfirmedField, $idField, $createdAt, $updatedAt, $loginAt]; + } +} diff --git a/src/Controller/Admin/ServiceCrudController.php b/src/Controller/Admin/ServiceCrudController.php new file mode 100644 index 0000000..63754c2 --- /dev/null +++ b/src/Controller/Admin/ServiceCrudController.php @@ -0,0 +1,94 @@ +setCurrency(null); // remove the default value which is not needed here + + return $product; + } + + public function configureFields(string $pageName): iterable + { + $panels = $this->getPanels(); + + [ + 'idField' => $idField, + 'typeField' => $typeField, + 'statusField' => $statusField, + 'visibilityField' => $visibilityField, + 'ownerField' => $ownerField, + 'categoryField' => $categoryField, + 'nameField' => $nameField, + 'descriptionField' => $descriptionField, + 'durationField' => $durationField, + 'createdAt' => $createdAt, + 'updatedAt' => $updatedAt, + ] = $this->getFields($pageName); + + // list + if ($pageName === Crud::PAGE_INDEX) { + return [$nameField, $ownerField, $categoryField, $statusField, $visibilityField, $createdAt]; + } + + // forms + if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { + /** @var ChoiceField $statusField */ + $statusField->setChoices(ProductStatus::cases()); + /** @var ChoiceField $visibilityField */ + $visibilityField->setChoices(ProductVisibility::cases()); + + return [$nameField, $ownerField, $categoryField, $statusField, $visibilityField, $descriptionField, $durationField]; + } + + // detail + + return [ + $panels['information'], + $ownerField, + $categoryField, + $statusField, + $visibilityField, + $nameField, + $descriptionField, + $durationField, + + $panels['tech_information'], + $idField, + $typeField, + $createdAt, + $updatedAt, + ]; + } +} diff --git a/src/Controller/Admin/ServiceRequestCrudController.php b/src/Controller/Admin/ServiceRequestCrudController.php new file mode 100755 index 0000000..15f74b2 --- /dev/null +++ b/src/Controller/Admin/ServiceRequestCrudController.php @@ -0,0 +1,169 @@ +setEntityLabelInSingular('Loan') + ->setPageTitle(Crud::PAGE_DETAIL, fn (ServiceRequest $serviceRequest) => $this->translator->trans( + 'loan.title', + [ + '%date%' => $serviceRequest->getStartAt()->format($this->translator->trans('format.date', [], 'date')), + '%lender%' => $serviceRequest->getOwner()->getDisplayName(), + '%borrower%' => $serviceRequest->getRecipient()->getDisplayName(), + ], + DashboardController::DOMAIN) + ) + ->setEntityLabelInPlural('loans') + ->setDefaultSort(['createdAt' => 'DESC']) + ->showEntityActionsInlined() + ; + } + + public function configureFilters(Filters $filters): Filters + { + return $filters + ->add(UuidFilter::new('id')) + ->add('owner') + ->add('product') + ->add('recipient') + ->add(EnumFilter::new('status', LoanStatusType::class)) + ; + } + + public function configureActions(Actions $actions): Actions + { + $conversation = Action::new('conversation') + ->linkToCrudAction('conversation') + ->setIcon('fas fa-comment-dots') + ->displayIf(fn () => $this->configurationRepository->getInstanceConfiguration()?->isConversationAdminAccessible()) + ; + + return $actions + ->add(Crud::PAGE_INDEX, Action::DETAIL) + ->remove(Crud::PAGE_INDEX, Action::EDIT) + ->remove(Crud::PAGE_INDEX, Action::NEW) + ->remove(Crud::PAGE_INDEX, Action::DELETE) + ->add(Crud::PAGE_DETAIL, $conversation) + ->remove(Crud::PAGE_DETAIL, Action::EDIT) + ->remove(Crud::PAGE_DETAIL, Action::DELETE) + ->update(Crud::PAGE_INDEX, Action::DETAIL, function (Action $action) { + return $action + ->setCssClass('btn btn-sm btn-primary') + ->setIcon('fa fa-search'); + }) + ; + } + + public static function getEntityFqcn(): string + { + return ServiceRequest::class; + } + + public function configureFields(string $pageName): iterable + { + $ownerField = AssociationField::new('owner'); + $recipientField = AssociationField::new('recipient'); + $productField = AssociationField::new('product'); + $productTypeField = AssociationField::new('product') + ->setTemplatePath('admin/loan/product_type_field.html.twig') + ->setLabel('type'); + $statusField = ChoiceField::new('status') + ->setFormType(EnumType::class) + ->setFormTypeOption('class', ServiceRequest::class) + ->setChoices(ServiceRequestStatus::getAsArray()); + $messageCountField = IntegerField::new('messagesCount'); + + $startAt = DateTimeField::new('startAt'); + $endAt = DateTimeField::new('endAt'); + $createdAt = DateTimeField::new('createdAt'); + + if ($pageName === Crud::PAGE_INDEX) { + return [$ownerField, $productTypeField, $productField, $recipientField, $statusField, $startAt, $endAt, $messageCountField, $createdAt]; + } + + // no new and edit page for this crud + + // show + $ownerField = AssociationField::new('owner') + ->setTemplatePath('admin/loan/user_field.html.twig') + ->setLabel(''); + $recipientField = AssociationField::new('recipient') + ->setTemplatePath('admin/loan/user_field.html.twig') + ->setLabel(''); + $productField = AssociationField::new('product') + ->setTemplatePath('admin/loan/product_field.html.twig') + ->setLabel(''); + + /** @var ServiceRequest $serviceRequest */ + $serviceRequest = $this->getContext()?->getEntity()->getInstance(); + $productPanel = FormField::addPanel($serviceRequest->getProduct()->getType()->value, 'fa-solid fa-box'); + $serviceRequestInformationPanel = FormField::addPanel('panel.loan_information', 'fas fa-info-circle'); + $ownerPanel = FormField::addPanel('panel.lender', 'fa-solid fa-user'); + $recipientPanel = FormField::addPanel('panel.borrower', 'fa-solid fa-user'); + + return [ + $serviceRequestInformationPanel, + $startAt, + $endAt, + $statusField, + $createdAt, + + $ownerPanel, + $ownerField, + + $recipientPanel, + $recipientField, + + $productPanel, + $productField, + ]; + } + + /** + * Entity is accesible thanks to the EA AdminContext. + */ + public function conversation(): Response + { + if (!$this->configurationRepository->getInstanceConfigurationOrCreate()->isConversationAdminAccessible()) { + throw new AccessDeniedHttpException(); + } + + return $this->render('admin/service_request/conversation.html.twig'); + } +} diff --git a/src/Controller/Admin/UserCrudController.php b/src/Controller/Admin/UserCrudController.php new file mode 100755 index 0000000..b8ddff9 --- /dev/null +++ b/src/Controller/Admin/UserCrudController.php @@ -0,0 +1,100 @@ + $idField, + 'emailField' => $emailField, + 'firstNameField' => $firstNameField, + 'lastNameField' => $lastNameField, + 'plainPassword' => $plainPassword, + 'enabledField' => $enabledField, + 'emailConfirmedField' => $emailConfirmedField, + 'loginAt' => $loginAt, + 'createdAt' => $createdAt, + 'updatedAt' => $updatedAt, + 'avatarField' => $avatarField, + 'phoneNumberField' => $phoneNumberField, + 'categoryField' => $categoryField, + 'descriptionField' => $descriptionField, + 'smsNotificationsField' => $smsNotificationsField, + 'vacationModeField' => $vacationModeField, + 'addressField' => $addressField, + ] = $this->getFields($pageName); + + if ($pageName === Crud::PAGE_INDEX) { + return [$emailField, $firstNameField, $lastNameField, $enabledField, $emailConfirmedField, $avatarField, $createdAt, $updatedAt, $loginAt]; + } + + $panels = $this->getPanels(); + + if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { + return [ + $panels['information'], + $emailField, + $firstNameField, + $lastNameField, + $avatarField, + $phoneNumberField, + $descriptionField, + $categoryField, + $smsNotificationsField, + $vacationModeField, + $plainPassword, + + $panels['tech_information'], + $enabledField, + $emailConfirmedField, + ]; + } + + return [ + $panels['information'], + $emailField, + $firstNameField, + $lastNameField, + $avatarField, + $phoneNumberField, + $descriptionField, + $addressField, + $categoryField, + $smsNotificationsField, + $vacationModeField, + + $panels['tech_information'], + $idField, + $enabledField, + $emailConfirmedField, + $createdAt, + $updatedAt, + $loginAt, + ]; + } +} diff --git a/src/Controller/Admin/UserGroupCrudController.php b/src/Controller/Admin/UserGroupCrudController.php new file mode 100755 index 0000000..437e765 --- /dev/null +++ b/src/Controller/Admin/UserGroupCrudController.php @@ -0,0 +1,260 @@ +setEntityLabelInPlural('user_groups') + ->setSearchFields(['group']) + ->setDefaultSort(['user' => 'ASC']) + ; + } + + public function configureFilters(Filters $filters): Filters + { + $filters + ->add(UuidFilter::new('id')) + ->add(MyUsersFilter::new('user')) + ->add(MyGroupFilter::new('group')) + ->add(EnumFilter::new('membership', UserMembershipType::class)) + ; + + if ($this->authorizationChecker->isAdmin()) { + $filters->add('mainAdminAccount'); + } + + return $filters; + } + + public function configureActions(Actions $actions): Actions + { + $currentUser = $this->security->getUser(); + $actions->update(Crud::PAGE_INDEX, 'delete', function (Action $action) use ($currentUser) { + return $action->displayIf(fn (UserGroup $usergroup) => $currentUser !== $usergroup->getUser() && !$usergroup->isMainAdminAccount()); + }); + + return $actions + ->add(Crud::PAGE_INDEX, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::DETAIL) + ->add(Crud::PAGE_EDIT, Action::INDEX) + ->remove(Crud::PAGE_INDEX, Action::NEW) + ; + } + + public static function getEntityFqcn(): string + { + return UserGroup::class; + } + + /** + * When a group admin is logged, we must restrict the groups he can access to. + * + * @see GroupCrubController + */ + public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder + { + // admins can see everything + $qb = parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters); + if ($this->authorizationChecker->isAdmin()) { + return $qb; + } + + /** @var User $user */ + $user = $this->getUser(); + $qb->andWhere(sprintf('%s.group IN (:groups)', $qb->getRootAliases()[0] ?? '')) + ->setParameter(':groups', $user->getMyGroupsAsAdmin()); + + return $qb; + } + + public function configureFields(string $pageName): iterable + { + /** @var User $user */ + $user = $this->getUser(); + + $idFIeld = IdField::new('id') + ->setLabel('id') + ->hideOnForm(); + + $userField = AssociationField::new('user'); + if (!$this->authorizationChecker->isAdmin()) { + $userField + ->setTemplatePath('easy_admin/field/user_email.html.twig') + ->setQueryBuilder(function (QueryBuilder $queryBuilder) use ($user) { + $queryBuilder + ->innerJoin('entity.userGroups', 'ug') + ->andWhere('ug.group IN (:groups)') + ->setParameter(':groups', $user->getMyGroupsAsAdmin()); + }); + } + + $groupField = AssociationField::new('group'); + if (!$this->authorizationChecker->isAdmin()) { + $groupField + ->setQueryBuilder(function (QueryBuilder $queryBuilder) use ($user) { + $queryBuilder + ->andWhere('entity.id IN (:groups)') + ->setParameter(':groups', $user->getMyGroupsAsAdmin()); + }); + } + + $membershipField = ChoiceField::new('membership') + ->setFormType(EnumType::class) + ->setFormTypeOption('class', UserMembership::class) + ->setChoices(UserMembership::getAsArray()); + $createdAt = DateTimeField::new('createdAt'); + $updatedAt = DateTimeField::new('updatedAt'); + $startAt = DateField::new('startAt'); + $endAt = DateField::new('endAt'); + $expiresInField = IntegerField::new('expiresIn') + ->formatValue(function ($value) { + return $value !== null ? $this->translator->trans($this->getI18nPrefix().'.expires_in.formatted_value', ['%days%' => $value], 'admin') : ''; + }) + ; + $payedAt = DateTimeField::new('payedAt'); + + $mainAdminAccountField = BooleanField::new('mainAdminAccount') + ->setTemplatePath('easy_admin/field/boolean_check_only.html.twig'); + + $panels = $this->getPanels(); + if ($pageName === Crud::PAGE_INDEX) { + return [$groupField, $userField, $membershipField, $mainAdminAccountField, $payedAt, $startAt, $endAt, $expiresInField, $createdAt, $updatedAt]; + } + + if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) { + $membershipField->setChoices(UserMembership::cases()); + $editionFields = [ + $groupField, + $userField, + $membershipField, + $startAt, + $endAt, + ]; + + if ($this->authorizationChecker->isAdmin()) { + $editionFields[] = $mainAdminAccountField; + } + + return $editionFields; + } + + // show + + return [ + $panels['information'], + $groupField, + $userField, + $membershipField, + $startAt, + $endAt, + $payedAt, + $mainAdminAccountField, + + $panels['tech_information'], + $idFIeld, + $updatedAt, + $createdAt, + ]; + } + + public function createEditForm(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormInterface + { + /** @var UserGroup $previousUserGroup */ + $previousUserGroup = clone $entityDto->getInstance(); + $this->previousUserGroup = $previousUserGroup; + + return parent::createEditForm($entityDto, $formOptions, $context); + } + + /** + * @throws TransportExceptionInterface + */ + public function updateEntity(EntityManagerInterface $entityManager, mixed $entityInstance): void + { + /** @var UserGroup $entityInstance */ + parent::updateEntity($entityManager, $entityInstance); + $user = $entityInstance->getUser(); + $membership = $entityInstance->getMembership(); + $context = []; + $context['group'] = $entityInstance->getGroup(); + $context['user'] = $user; + + // don't send both emails + if ($entityInstance->isMainAdminAccount() && !$this->previousUserGroup->isMainAdminAccount()) { + $this->mailer->send(MainAdminPromotionEmail::class, $context); + $this->sendSms($user, MainAdminPromotionEmail::class); + } elseif ($membership !== $this->previousUserGroup->getMembership() && $membership->isAdmin()) { + $this->mailer->send(AdminPromotionEmail::class, $context); + $this->sendSms($user, AdminPromotionEmail::class); + } + } +} diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php new file mode 100644 index 0000000..6958e73 --- /dev/null +++ b/src/Controller/AppController.php @@ -0,0 +1,48 @@ +getPreferredLanguage($this->getParameter('kernel.enabled_locales')); + + return $this->redirectToRoute('home', ['_locale' => $locale]); + } + + #[Route(path: ['en' => '/en', 'fr' => '/fr'], name: 'home')] + public function home(): Response + { + $page = $this->pageRepository->getHome(); + + return $this->render('cms/page.html.twig', [ + 'page' => $page, + 'is_home' => true, + ]); + } + + #[Route(path: '/ping', name: 'ping')] + #[Route(path: '/healthz', name: 'healthz')] + public function ping(): Response + { + return $this->json('OK'); + } +} diff --git a/src/Controller/Cms/CmsController.php b/src/Controller/Cms/CmsController.php new file mode 100644 index 0000000..44524e2 --- /dev/null +++ b/src/Controller/Cms/CmsController.php @@ -0,0 +1,38 @@ +pageRepository->findOneBySlug($slug); + if ($page === null || !$page->isEnabled()) { + throw $this->createNotFoundException('Page not found.'); + } + + return $this->render('cms/page.html.twig', compact('page')); + } +} diff --git a/src/Controller/FlashTrait.php b/src/Controller/FlashTrait.php new file mode 100644 index 0000000..21ae7d5 --- /dev/null +++ b/src/Controller/FlashTrait.php @@ -0,0 +1,18 @@ +addFlash('success', $message); + } + + public function addFlashWarning(string $message): void + { + $this->addFlash('warning', $message); + } +} diff --git a/src/Controller/Group/CreateGroupAction.php b/src/Controller/Group/CreateGroupAction.php new file mode 100644 index 0000000..45ce1d3 --- /dev/null +++ b/src/Controller/Group/CreateGroupAction.php @@ -0,0 +1,93 @@ + MyAccountAction::BASE_URL_EN.'/groups/create-my-group', + 'fr' => MyAccountAction::BASE_URL_FR.'/groupes/creer-mon-groupe', + ], + name: 'create', + )] + public function createGroup(Request $request, #[CurrentUser] User $user): Response + { + $configuration = $this->configurationRepository->getInstanceConfigurationOrCreate(); + if (!$configuration->isGroupsCreationForAll()) { + throw $this->createAccessDeniedException('Cannot create group with current settings.'); + } + + $newGroup = (new Group())->setInvitationByAdmin(true); + $form = $this->createForm(CreateGroupFormType::class, $newGroup)->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->groupRepository->save($newGroup, true); + $newAdminGroup = UserGroup::newUserGroup($user, $newGroup); + $this->userGroupRepository->save($newAdminGroup, true); + + // force login to refresh the roles so the user can access its group + // in the group admin interface + $this->doctrine->refresh($user); + $this->security->login($user); + + return $this->redirect( + $this->adminUrlGenerator + ->setController(GroupCrudController::class) + ->setEntityId($newGroup->getId()) + ->set('crudAction', Crud::PAGE_EDIT) + ->generateUrl() + ); + } + + return $this->render('pages/group/create.html.twig', compact('form')); + } +} diff --git a/src/Controller/Group/GroupController.php b/src/Controller/Group/GroupController.php new file mode 100644 index 0000000..f91e8df --- /dev/null +++ b/src/Controller/Group/GroupController.php @@ -0,0 +1,140 @@ + '/{_locale}/groups', + 'fr' => '/{_locale}/groupes', + ], + name: 'list' + )] + public function list(Request $request, #[CurrentUser] ?User $user): Response + { + $page = $this->getPage($request); + $form = $this->createForm(GroupSelectFormType::class)->handleRequest($request); + $groupName = null; + if ($form->isSubmitted() && $form->isValid()) { + /** @var string $groupName */ + $groupName = $form->get('q')->getData(); + } + + /** @var Query $query */ + $query = $this->queryBus->query(new GetGroupsQuery($user, $groupName)); + $pagination = $this->paginator->paginate($query, $page, self::MAX_ELEMENT_BY_PAGE); + + return $this->render('pages/group/list.html.twig', compact('pagination', 'form')); + } + + /** + * The slug is only for SEO. + */ + #[Route([ + 'en' => '/{_locale}/groups/{slug}/{id}', + 'fr' => '/{_locale}/groupes/{slug}/{id}', + ], + name: 'show', + requirements: ['slug' => Requirement::ASCII_SLUG, 'id' => Requirement::UUID_V6] + )] + public function show(string $id, #[CurrentUser] ?User $user): Response + { + $group = $this->getGroup($id); + $showQuitGroupChoices = $this->getShowQuitGroupsChoices($group, $user); + + return $this->render('pages/group/show.html.twig', compact('group', 'showQuitGroupChoices')); + } + + /** + * This is the same route as show but it requires to be logged. It is useful to + * redirect the user to this page if he isn't logged, so he can accept a pending + * invitation for example. + */ + #[IsGranted(User::ROLE_USER)] + #[Route([ + 'en' => '/en/groups/{slug}/{id}/invitation', + 'fr' => '/fr/groupes/{slug}/{id}/invitation', + ], + name: 'show_logged', + requirements: ['slug' => Requirement::ASCII_SLUG, 'id' => Requirement::UUID_V6] + )] + public function showLogged(string $id, #[CurrentUser] ?User $user): Response + { + $group = $this->getGroup($id); + $showQuitGroupChoices = $this->getShowQuitGroupsChoices($group, $user); + + return $this->render('pages/group/show.html.twig', compact('group', 'showQuitGroupChoices')); + } + + private function getShowQuitGroupsChoices(Group $group, ?User $user): bool + { + return $this->productManager->hasProductsOnlyInGroup($group, $user); + } + + #[Route([ + 'en' => '/{_locale}/groups/{slug}/{id}/members', + 'fr' => '/{_locale}/groupes/{slug}/{id}/membres', + ], + name: 'members', + requirements: ['slug' => Requirement::ASCII_SLUG, 'id' => Requirement::UUID_V6] + )] + public function showMembers(Request $request, string $id): Response + { + $group = $this->getGroup($id); + $page = $this->getPage($request); + $form = $this->createForm(GroupSelectFormType::class)->handleRequest($request); + + $memberName = null; + if ($form->isSubmitted() && $form->isValid()) { + /** @var string $memberName */ + $memberName = $form->get('q')->getData(); + } + + /** @var Query $query */ + $query = $this->queryBus->query(new GetGroupMembersQuery(Uuid::fromString($id), $memberName)); + $pagination = $this->paginator->paginate($query, $page, self::MAX_ELEMENT_BY_PAGE); + + return $this->render('pages/group/members.html.twig', compact('pagination', 'form', 'group')); + } +} diff --git a/src/Controller/Group/GroupTrait.php b/src/Controller/Group/GroupTrait.php new file mode 100644 index 0000000..193feb5 --- /dev/null +++ b/src/Controller/Group/GroupTrait.php @@ -0,0 +1,25 @@ +queryBus->query(new GetGroupByIdQuery(Uuid::fromString($id))); + } catch (HandlerFailedException $e) { + throw $this->createNotFoundException($e->getMessage()); + } + + return $group; + } +} diff --git a/src/Controller/Menu/MenuController.php b/src/Controller/Menu/MenuController.php new file mode 100644 index 0000000..8998ba6 --- /dev/null +++ b/src/Controller/Menu/MenuController.php @@ -0,0 +1,77 @@ +render( + 'components/layout/_navbar.html.twig', + $this->menuItems($q) + ); + } + + /** + * @return array + * + * @throws NonUniqueResultException + */ + private function menuItems(?string $q): array + { + $menu = $this->menuRepository->getByCode(Menu::MENU); + $firstItems = $this->menuItemRepository->findFirstLevelMenuLinks(Menu::MENU); + $configuration = $this->configurationRepository->getInstanceConfiguration(); + Assert::isInstanceOf($configuration, Configuration::class); + + return [ + 'menu' => $menu, + 'items' => $firstItems, + 'contactEnabled' => $configuration->getContactEnabled(), + 'contactEmail' => $configuration->getContactEmail(), + 'q' => $q, + ]; + } + + /** + * @throws NonUniqueResultException + */ + public function footerItems(): Response + { + $menu = $this->menuRepository->getByCode(Menu::FOOTER); + $links = $this->menuItemRepository->getFooterItems(LinkType::LINK->value); + $icons = $this->menuItemRepository->getFooterItems(LinkType::SOCIAL_NETWORK->value); + + return $this->render( + 'components/layout/_footer.html.twig', + [ + 'menu' => $menu, + 'links' => $links, + 'icons' => $icons, + ] + ); + } +} diff --git a/src/Controller/PaginationTrait.php b/src/Controller/PaginationTrait.php new file mode 100644 index 0000000..19d8529 --- /dev/null +++ b/src/Controller/PaginationTrait.php @@ -0,0 +1,34 @@ + + * + * @return PaginationInterface + */ + private function paginate(SearchResult $searchResult): PaginationInterface + { + return $this->paginator->paginate( + $searchResult, + (int) $searchResult->getPage(), + (int) $searchResult->getHitsPerPage() + ); + } +} diff --git a/src/Controller/Payment/DoneAction.php b/src/Controller/Payment/DoneAction.php new file mode 100755 index 0000000..13e2d5d --- /dev/null +++ b/src/Controller/Payment/DoneAction.php @@ -0,0 +1,76 @@ + Requirement::UUID_V6], + )] + public function __invoke(Request $request, string $id, #[CurrentUser] User $user): Response + { + $groupOffer = $this->getGroupOffer($id); + + try { + /** @var PaymentToken $token */ + $token = $this->payum->getHttpRequestVerifier()->verify($request); + } catch (\Exception) { + throw new UnprocessableEntityHttpException('Cannot verify Payum token.'); + } + + /** @var GetHumanStatus $status */ + $status = $this->commandBus->dispatch(new DoneCommand($groupOffer->getId(), $user->getId(), $token)); + if ($status->isCaptured()) { + $this->addFlashSuccess($this->translator->trans($this->getI18nPrefix().'.flash.success', [ + '%group%' => $groupOffer->getGroup()->getName()], + )); + } else { + $this->addFlashWarning($this->translator->trans($this->getI18nPrefix().'.status.'.$status->getValue())); + } + + return $this->redirectToRoute('app_group_show', $groupOffer->getGroup()->getRoutingParameters()); + } +} diff --git a/src/Controller/Payment/GroupOfferTrait.php b/src/Controller/Payment/GroupOfferTrait.php new file mode 100644 index 0000000..c8b623b --- /dev/null +++ b/src/Controller/Payment/GroupOfferTrait.php @@ -0,0 +1,25 @@ +groupOfferRepository->find(Uuid::fromString($id)); + if ($groupOffer === null || !$groupOffer->isActive()) { + throw $this->createNotFoundException('Group offer not found'); + } + + return $groupOffer; + } +} diff --git a/src/Controller/Payment/PrepareAction.php b/src/Controller/Payment/PrepareAction.php new file mode 100755 index 0000000..af5e4bc --- /dev/null +++ b/src/Controller/Payment/PrepareAction.php @@ -0,0 +1,66 @@ + Requirement::UUID_V6], + methods: ['POST'], + )] + public function __invoke(Request $request, string $id, #[CurrentUser] User $user): Response + { + $groupOffer = $this->getGroupOffer($id); + + /** @var ?string $token */ + $token = $request->request->get('token'); + if (!$this->isCsrfTokenValid('payment_prepare', $token)) { + throw new UnprocessableEntityHttpException('Invalid CSRF token'); + } + + // create and save the payment main reference + $payment = $this->payumManager->getPayment($groupOffer, $user); + + // create the capture token and redirect to the capture action + $captureToken = $this->payumManager->getCaptureToken($payment, DoneAction::ROUTE_NAME, [ + 'id' => $id, + ]); + + return $this->redirect($captureToken->getTargetUrl()); + } +} diff --git a/src/Controller/Product/ProductController.php b/src/Controller/Product/ProductController.php new file mode 100644 index 0000000..fdd5f30 --- /dev/null +++ b/src/Controller/Product/ProductController.php @@ -0,0 +1,91 @@ + '/{_locale}/product', + 'fr' => '/{_locale}/produits', + ], + name: 'list' + )] + public function list(Request $request, #[CurrentUser] ?User $user): Response + { + $page = $this->getPage($request); + $q = u($request->query->get('q'))->toString(); + $searchDto = new Search($q, $page, $user); + + // The DTO is modified with selected values if the form is submitted and valid + $searchForm = $this->createForm(SearchFormType::class, $searchDto)->handleRequest($request); + + return $this->render('pages/product/list.html.twig', [ + 'objects_pagination' => $this->paginate($this->meilisearch->searchObjects($searchDto)), + 'services_pagination' => $this->paginate($this->meilisearch->searchServices($searchDto)), + 'search_form' => $searchForm, + ]); + } + + /** + * The slug is only for SEO. + */ + #[Route([ + 'en' => '/{_locale}/product/{slug}/{id}', + 'fr' => '/{_locale}/produits/{slug}/{id}', + ], + name: 'show', + requirements: ['slug' => Requirement::ASCII_SLUG, 'id' => Requirement::UUID_V6] + )] + public function show(string $slug, string $id): Response + { + try { + /** @var Product $product */ + $product = $this->queryBus->query(new GetProductByIdQuery(Uuid::fromString($id))); + } catch (HandlerFailedException $e) { + throw $this->createNotFoundException($e->getMessage()); + } + + return $this->render('pages/product/show.html.twig', compact('slug', 'id', 'product')); + } +} diff --git a/src/Controller/RequestTrait.php b/src/Controller/RequestTrait.php new file mode 100644 index 0000000..72167f1 --- /dev/null +++ b/src/Controller/RequestTrait.php @@ -0,0 +1,26 @@ +query->getInt($key, 1); + $page = max($page, 1); // no negative page or 0 + + // limit max page to 100000 (2 million products) + + return min($page, 100000); + } +} diff --git a/src/Controller/Security/AccountCreateController.php b/src/Controller/Security/AccountCreateController.php new file mode 100644 index 0000000..c541fff --- /dev/null +++ b/src/Controller/Security/AccountCreateController.php @@ -0,0 +1,126 @@ +i18nPrefix = $this->getI18nPrefix(); + } + + /** + * @see AccountCreateStep1CommandHandler + */ + #[Route(path: [ + 'en' => '/{_locale}/account/create-my-account', + 'fr' => '/{_locale}/compte/creer-mon-compte', + ], name: 'security_account_create_step1')] + public function createStep1(Request $request, string $_route): Response + { + $form = $this->createForm(AccountCreateStep1FormType::class)->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + /** @var User $newUser */ + $newUser = $form->getData(); + $this->commandBus->dispatch(new AccountCreateStep1Command($newUser)); + $this->addFlashSuccess($this->i18nPrefix.'.step1.flash.success'); + + return $this->redirectToRoute($_route); + } + + return $this->render('pages/register/step1.html.twig', compact('form')); + } + + /** + * @see AccountCreateStep2CommandHandler + */ + #[Route(path: [ + 'en' => '/{_locale}/account/create-my-account-step2/{token}', + 'fr' => '/{_locale}/compte/creer-mon-compte-etape-2/{token}', + ], name: 'security_account_create_step2')] + public function createStep2(Request $request, string $token): Response + { + try { + /** @var User $user */ + $user = $this->queryBus->query(new GetUserByTokenQuery($token)); + } catch (HandlerFailedException $e) { + /** @var \Exception $exception */ + $exception = $e->getPrevious(); + switch ($exception::class) { + case UserNotFoundException::class: + $this->addFlashWarning($this->i18nPrefix.'.step2.user_not_found.warning'); + break; + + case UserConfirmationTokenExpiredException::class: + // send a new confirmation email with a new token + $this->commandBus->dispatch(new AccountCreateStep2RefreshCommand($exception->id)); + $this->addFlashWarning($this->i18nPrefix.'.step2.user_confirmation_token_expired.warning'); + break; + } + + return $this->redirectToRoute('app_login'); + } + + // nominal case: user found and token not expired + $form = $this->createForm(AccountCreateStep2FormType::class, $user->setStep2Defaults())->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + /** @var User $user */ + $user = $form->getData(); + $this->commandBus->dispatch(new AccountCreateStep2Command($user)); + $this->security->login($user); // auto-log the user + + // If user has pending invitations then redirect them to the first group + // found without doing the confirmation stuff, it must be done on the + // page group. + $group = $user->getMyGroupsAsInvited()->first(); + if ($group !== false) { + $this->addFlashSuccess($this->i18nPrefix.'.step2.with_invitation.flash.success'); + + return $this->redirectToRoute('app_group_show_logged', $group->getRoutingParameters()); + } + + // otherwise go to the address form + $this->addFlashSuccess($this->i18nPrefix.'.step2.flash.success'); + + return $this->redirectToRoute(MyAccountAction::ROUTE); + } + + return $this->render('pages/register/step2.html.twig', compact('form', 'user')); + } +} diff --git a/src/Controller/Security/LostPasswordAction.php b/src/Controller/Security/LostPasswordAction.php new file mode 100644 index 0000000..8006b53 --- /dev/null +++ b/src/Controller/Security/LostPasswordAction.php @@ -0,0 +1,43 @@ + '/{_locale}/account/lost-password', + 'fr' => '/{_locale}/compte/mot-de-passe-oublie', + ], name: 'security_lost_password')] + public function __invoke(Request $request, string $_route): Response + { + $form = $this->createForm(LostPasswordFormType::class)->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + /** @var LostPasswordCommand $lostPasswordCommand */ + $lostPasswordCommand = $form->getData(); + $this->commandBus->dispatch($lostPasswordCommand); + $this->addFlashSuccess('lost_password.form.success'); + + return $this->redirectToRoute($_route); + } + + return $this->render('pages/password/lost.html.twig', compact('form')); + } +} diff --git a/src/Controller/Security/ResetPasswordAction.php b/src/Controller/Security/ResetPasswordAction.php new file mode 100644 index 0000000..8686757 --- /dev/null +++ b/src/Controller/Security/ResetPasswordAction.php @@ -0,0 +1,70 @@ + '/{_locale}/account/password-reset/{token}', + 'fr' => '/{_locale}/compte/reinitialisation-mot-de-passe/{token}', + ], name: 'security_reset_password')] + public function __invoke(Request $request, string $token, string $_route): Response + { + try { + /** @var User $user */ + $user = $this->queryBus->query(new ResetPasswordQuery($token)); + } catch (HandlerFailedException $e) { + /** @var \Exception $exception */ + $exception = $e->getPrevious(); + + if ($exception::class === UserNotFoundException::class) { + $this->addFlashWarning('reset_password.user_not_found.exception'); + } + + // token expired, the user must renew its request + if ($exception::class === UserLostPasswordTokenExpiredException::class) { + $this->addFlashWarning('reset_password.user_lostpassword_token_expired.exception'); + } + + return $this->redirectToRoute('security_lost_password'); + } + + $form = $this->createForm(ResetPasswordFormType::class)->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + /** @var ResetPasswordCommand $resetPasswordCommand */ + $resetPasswordCommand = $form->getData(); + $resetPasswordCommand->id = $user->getId(); + $this->commandBus->dispatch($resetPasswordCommand); + $this->addFlashSuccess('reset_password.form.success'); + + return $this->redirectToRoute('app_login'); + } + + return $this->render('pages/password/reset.html.twig', compact('form')); + } +} diff --git a/src/Controller/Security/SecurityController.php b/src/Controller/Security/SecurityController.php new file mode 100644 index 0000000..8390f01 --- /dev/null +++ b/src/Controller/Security/SecurityController.php @@ -0,0 +1,28 @@ +getLastAuthenticationError(); + + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('pages/login.html.twig', [ + 'last_username' => $lastUsername, + 'error' => $error, + ]); + } +} diff --git a/src/Controller/SecurityTrait.php b/src/Controller/SecurityTrait.php new file mode 100644 index 0000000..716a197 --- /dev/null +++ b/src/Controller/SecurityTrait.php @@ -0,0 +1,28 @@ +getUser(); + Assert::isInstanceOf($user, User::class, 'This function should only be called in an authenticated context (#[isGranted(User::ROLE_)])'); + /** @var User $user */ + + return $user; + } +} diff --git a/src/Controller/User/Account/ChangeLoginAction.php b/src/Controller/User/Account/ChangeLoginAction.php new file mode 100644 index 0000000..fd500a5 --- /dev/null +++ b/src/Controller/User/Account/ChangeLoginAction.php @@ -0,0 +1,62 @@ + MyAccountAction::BASE_URL_EN.'/my-email', + 'fr' => MyAccountAction::BASE_URL_FR.'/mon-email', + ], name: 'app_user_change_login')] + public function __invoke(Request $request, #[CurrentUser] User $user): Response + { + $form = $this->createForm(ChangeLoginFormType::class, $user)->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $this->addFlashSuccess($this->getI18nPrefix().'.flash.success'); + /** @var string $email */ + $email = $form->get('email')->getData(); + $command = new ChangeLoginCommand($user->getId(), $email); + $this->commandBus->dispatch($command); + + return $this->redirectToRoute(MyAccountAction::ROUTE); + } + + // In case of error, we must reload the original email + $this->entityManager->refresh($user); + + return $this->render('pages/user/account/change_login.html.twig', compact('form')); + } +} diff --git a/src/Controller/User/Account/ChangePasswordAction.php b/src/Controller/User/Account/ChangePasswordAction.php new file mode 100644 index 0000000..933c554 --- /dev/null +++ b/src/Controller/User/Account/ChangePasswordAction.php @@ -0,0 +1,56 @@ + MyAccountAction::BASE_URL_EN.'/my-password', + 'fr' => MyAccountAction::BASE_URL_FR.'/mon-mot-de-passe', + ], name: 'app_user_change_password')] + public function __invoke(Request $request, #[CurrentUser] User $user): Response + { + $form = $this->createForm(ChangePasswordFormType::class)->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + /** @var string $plaintextPassword */ + $plaintextPassword = $form->get('plainPassword')->getData(); + + $this->userManager->updatePassword($user->setPlainPassword($plaintextPassword)); + $this->userRepository->save($user, true); + + $this->addFlashSuccess($this->getI18nPrefix().'.flash.success'); + + return $this->redirectToRoute('app_user_my_account'); + } + + return $this->render('pages/user/account/change_password.html.twig', compact('form')); + } +} diff --git a/src/Controller/User/Account/DeleteUserAvatarAction.php b/src/Controller/User/Account/DeleteUserAvatarAction.php new file mode 100644 index 0000000..ea136a0 --- /dev/null +++ b/src/Controller/User/Account/DeleteUserAvatarAction.php @@ -0,0 +1,47 @@ + Requirement::UUID_V6, + ] + )] + public function __invoke(#[CurrentUser] User $user): Response + { + $this->userManager->deleteAvatar($user); + $this->addFlashSuccess($this->getI18nPrefix().'.flash.success'); + + return $this->redirectToRoute('app_user_edit_profile'); + } +} diff --git a/src/Controller/User/Account/EditProfileAction.php b/src/Controller/User/Account/EditProfileAction.php new file mode 100644 index 0000000..9380580 --- /dev/null +++ b/src/Controller/User/Account/EditProfileAction.php @@ -0,0 +1,63 @@ + MyAccountAction::BASE_URL_EN.'/edit', + 'fr' => MyAccountAction::BASE_URL_FR.'/editer', + ], name: 'app_user_edit_profile')] + public function __invoke(Request $request, #[CurrentUser] User $user): Response + { + $form = $this->createForm(EditProfileFormType::class, $user)->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + /** @var UploadedFile|null $avatar */ + $avatar = $form->get('avatar')->getData(); + $this->userManager->upload($avatar, $user); + $this->userRepository->save($user, true); + $this->addFlashSuccess($this->getI18nPrefix().'.flash.success'); + + return $this->redirectToRoute(MyAccountAction::ROUTE); + } + + // In case of error, we must reload the original firstname (to display it in navbar) + $this->entityManager->refresh($user); + + return $this->render('pages/user/account/edit_profile.html.twig', compact('form')); + } +} diff --git a/src/Controller/User/Account/ProfileAction.php b/src/Controller/User/Account/ProfileAction.php new file mode 100644 index 0000000..9e9b6c5 --- /dev/null +++ b/src/Controller/User/Account/ProfileAction.php @@ -0,0 +1,64 @@ + '/{_locale}/user/{userId}', + 'fr' => '/{_locale}/utilisateur/{userId}', + ], + name: 'app_user_profile', + requirements: ['slug' => Requirement::ASCII_SLUG, 'id' => Requirement::UUID_V6, 'userId' => Requirement::UUID_V6] + )] + public function __invoke(Request $request, string $userId, #[CurrentUser] ?User $currentUser): Response + { + try { + /** @var User $user */ + $user = $this->queryBus->query(new GetUserQuery(Uuid::fromString($userId))); + } catch (HandlerFailedException $e) { + throw $this->createNotFoundException($e->getMessage()); + } + + $searchDto = new Search('', $this->getPage($request), $currentUser); + $searchDto->place = $user; + + return $this->render('pages/user/account/profile.html.twig', [ + 'user' => $user, + 'objects_pagination' => $this->paginate($this->meilisearch->searchObjects($searchDto)), + 'services_pagination' => $this->paginate($this->meilisearch->searchServices($searchDto)), + ]); + } +} diff --git a/src/Controller/User/AddressController.php b/src/Controller/User/AddressController.php new file mode 100644 index 0000000..79e6c0a --- /dev/null +++ b/src/Controller/User/AddressController.php @@ -0,0 +1,132 @@ + MyAccountAction::BASE_URL_EN.'/my-address/step-1', + 'fr' => MyAccountAction::BASE_URL_FR.'/mon-adresse/etape-1', + ], name: self::STEP1_ROUTE)] + public function step1(Request $request, SessionInterface $session): Response + { + $form = $this->createForm(AddressStep1FormType::class, $this->getAppUser()->getAddress())->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + /** @var Address $address */ + $address = $form->getData(); + $userAddressQuery = new UserAddressQuery($address); + + /** @var AddressCollection $addresses */ + $addresses = $this->queryBus->query($userAddressQuery); + if ($addresses->isEmpty()) { + $this->addFlashWarning('address.step1_action.no_address.warning'); + + return $this->render('pages/account/address/step1.html.twig', compact('form')); + } + + $userAddressStep1Data = new UserAddressStep1Data($address, $addresses); + $this->saveStep1Data($session, $userAddressStep1Data); + + return $this->redirectToRoute(self::STEP2_ROUTE); + } + + return $this->render('pages/account/address/step1.html.twig', compact('form')); + } + + #[isGranted(User::ROLE_USER)] + #[Route(path: [ + 'en' => MyAccountAction::BASE_URL_EN.'/my-address/step-2', + 'fr' => MyAccountAction::BASE_URL_FR.'/mon-adresse/etape-2', + ], name: self::STEP2_ROUTE)] + public function step2(Request $request, SessionInterface $session): Response + { + // direct access is forbidden or empty addresses (should not happen) + $userAddressStep1Data = $this->getStep1Data($session); + if ($userAddressStep1Data === null || $userAddressStep1Data->addresses->isEmpty()) { + return $this->redirectToRoute(self::STEP1_ROUTE); + } + + // give the form the address choices to use + $options = ['addresses' => $userAddressStep1Data->getAddressesAsArray()]; + $form = $this->createForm(AddressStep2FormType::class, null, $options)->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + /** @var NominatimAddress $address */ + $address = $form->get('addresses')->getData(); // the selected address + $command = new UpdateAddressCommand($this->getAppUser()->getId(), $userAddressStep1Data->address, $address); + $this->commandBus->dispatch($command); + $this->resetStep1Data($session); + $this->addFlashSuccess('address.step2_action.form.success'); + + return $this->redirectToRoute(MyAccountAction::ROUTE); + } + + $parameters = $userAddressStep1Data->getData(); + $parameters['form'] = $form; + + return $this->render('pages/account/address/step2.html.twig', $parameters); + } + + /** + * Save data for step2 so we already have all we need to create the form and + * save the data. + */ + private function saveStep1Data(SessionInterface $session, UserAddressStep1Data $userAddressStep1Data): void + { + $session->set(self::STEP1_DATA_KEY, $userAddressStep1Data); + } + + private function getStep1Data(SessionInterface $session): ?UserAddressStep1Data + { + /** @var ?UserAddressStep1Data $userAddressStep1Data */ + $userAddressStep1Data = $session->get(self::STEP1_DATA_KEY); + + return $userAddressStep1Data; + } + + private function resetStep1Data(SessionInterface $session): void + { + $session->set(self::STEP1_DATA_KEY, null); + } +} diff --git a/src/Controller/User/Group/UserGroupController.php b/src/Controller/User/Group/UserGroupController.php new file mode 100644 index 0000000..d93d672 --- /dev/null +++ b/src/Controller/User/Group/UserGroupController.php @@ -0,0 +1,128 @@ +getGroup($id); + + /** @var ?string $token */ + $token = $request->request->get('token'); + if (!$this->isCsrfTokenValid($tokenId, $token)) { + throw new UnprocessableEntityHttpException('Invalid CSRF token'); + } + + return $group; + } + + private function redirectToGroup(Group $group): RedirectResponse + { + return $this->redirectToRoute('app_group_show', $group->getRoutingParameters()); + } + + #[Route( + path: MyAccountAction::BASE_URL_EN.'/groups/{id}/join', + name: 'app_user_group_join', + requirements: ['id' => Requirement::UUID_V6], + methods: ['POST'], + )] + public function join(Request $request, string $id, #[CurrentUser] User $user): Response + { + $group = $this->getGroupAndCheckToken($request, $id, 'join_group'); + $command = new JoinGroupCommand($group->getId(), $user->getId()); + $this->commandBus->dispatch($command); + $this->addFlashSuccess($this->getI18nPrefix().'.flash.success'); + + return $this->redirectToGroup($group); + } + + /** + * @see UserGroupAcceptInvitationActionTest + */ + #[Route( + path: MyAccountAction::BASE_URL_EN.'/groups/{id}/acceptInvitation', + name: 'app_user_group_accept_invitation', + requirements: ['id' => Requirement::UUID_V6], + methods: ['POST'], + )] + public function acceptInvitation(Request $request, string $id, #[CurrentUser] User $user): Response + { + $group = $this->getGroupAndCheckToken($request, $id, 'accept_invitation'); + $command = new AcceptGroupInvitationCommand($group->getId(), $user->getId()); + $this->commandBus->dispatch($command); + $this->addFlashSuccess($this->getI18nPrefix().'.accept_invitation.flash.success'); + + return $this->redirectToGroup($group); + } + + /** + * @see UserGroupQuitGroupActionTest + */ + #[Route( + path: MyAccountAction::BASE_URL_EN.'/groups/{id}/quitGroup', + name: 'app_user_group_quit_group', + requirements: ['id' => Requirement::UUID_V6], + methods: ['POST'], + )] + public function quitGroup(Request $request, string $id, #[CurrentUser] User $user): Response + { + $group = $this->getGroupAndCheckToken($request, $id, 'quit_group'); + $type = $request->request->getAlpha('type'); + $command = new QuitGroupCommand($group->getId(), $user->getId(), $type); + $this->commandBus->dispatch($command); + $this->addFlashSuccess($this->getI18nPrefix().'.quit_group.flash.success'); + + return $this->redirectToGroup($group); + } + + #[Route(path: [ + 'en' => MyAccountAction::BASE_URL_EN.'/my-groups', + 'fr' => MyAccountAction::BASE_URL_FR.'/mes-groupes', + ], name: 'app_user_groups' + )] + public function list(): Response + { + return $this->render('pages/user/group/list.html.twig'); + } +} diff --git a/src/Controller/User/MyAccountAction.php b/src/Controller/User/MyAccountAction.php new file mode 100644 index 0000000..8808169 --- /dev/null +++ b/src/Controller/User/MyAccountAction.php @@ -0,0 +1,52 @@ + self::BASE_URL_EN, + 'fr' => self::BASE_URL_FR, + ], name: self::ROUTE)] + public function __invoke(#[CurrentUser] User $user): Response + { + $userHasNewLendingMessage = $this->messageRepository->userHasNewMessage($user, true); + $userHasNewLoanMessage = $this->messageRepository->userHasNewMessage($user, false); + $configuration = $this->configurationRepository->getInstanceConfigurationOrCreate(); + + // we can create a group if the settings for all is activated or an administrator + $canCreateGroup = $configuration->isGroupsCreationForAll() || $user->isAdmin(); + $contactEmail = $configuration->getContactEmail(); + + return $this->render('pages/account/index.html.twig', compact('userHasNewLoanMessage', 'userHasNewLendingMessage', 'canCreateGroup', 'contactEmail')); + } +} diff --git a/src/Controller/User/Product/DeleteProductAction.php b/src/Controller/User/Product/DeleteProductAction.php new file mode 100644 index 0000000..5807855 --- /dev/null +++ b/src/Controller/User/Product/DeleteProductAction.php @@ -0,0 +1,62 @@ + Requirement::UUID_V6, + ] + )] + public function __invoke(string $id): Response + { + try { + /** @var Product $product */ + $product = $this->queryBus->query(new GetProductByIdQuery(Uuid::fromString($id), ProductVoter::DELETE)); + } catch (HandlerFailedException $e) { + throw $this->createNotFoundException($e->getMessage()); + } + + $this->productManager->save($product->delete(), true); + $this->addFlashSuccess($this->getI18nPrefix().'.flash.success'); + + return $this->redirectToRoute(MyAccountAction::ROUTE); + } +} diff --git a/src/Controller/User/Product/DeleteProductPhotoAction.php b/src/Controller/User/Product/DeleteProductPhotoAction.php new file mode 100644 index 0000000..5f92a11 --- /dev/null +++ b/src/Controller/User/Product/DeleteProductPhotoAction.php @@ -0,0 +1,61 @@ + Requirement::UUID_V6, + ] + )] + public function __invoke(string $productId, string $image): Response + { + try { + /** @var Product $product */ + $product = $this->queryBus->query(new GetProductByIdQuery(Uuid::fromString($productId), ProductVoter::EDIT)); + } catch (HandlerFailedException $e) { + throw $this->createNotFoundException($e->getMessage()); + } + $this->productManager->deleteImage($product, $image); + $this->addFlashSuccess($this->getI18nPrefix().'.flash.success'); + + return $this->redirectToRoute('app_'.$product->getType()->value.'_edit', ['id' => $product->getId()]); + } +} diff --git a/src/Controller/User/Product/DeleteProductUnavailabilityAction.php b/src/Controller/User/Product/DeleteProductUnavailabilityAction.php new file mode 100644 index 0000000..22b63c7 --- /dev/null +++ b/src/Controller/User/Product/DeleteProductUnavailabilityAction.php @@ -0,0 +1,59 @@ + MyAccountAction::BASE_URL_EN.'/products/unavailability/{id}/delete', + 'fr' => MyAccountAction::BASE_URL_FR.'/produits/indisponibilite/{id}/supprimer', + ], + name: 'app_user_product_delete_availability', + requirements: [ + 'id' => Requirement::UUID_V6, + ] + )] + public function __invoke(string $id): Response + { + $productUnavailability = $this->productAvailabilityRepository->get($id); + + if (!$this->security->isGranted(ProductVoter::EDIT, $productUnavailability->getProduct())) { + throw new AccessDeniedHttpException('Unauthorized to delete this product unavailibility'); + } + + $this->productAvailabilityRepository->deleteProductUnavailability($productUnavailability); + $this->addFlashSuccess($this->getI18nPrefix().'.flash.success'); + + return $this->redirectToRoute(ProductAvailabilityController::ROUTE, ['id' => $productUnavailability->getProduct()->getId()]); + } +} diff --git a/src/Controller/User/Product/DuplicateProductAction.php b/src/Controller/User/Product/DuplicateProductAction.php new file mode 100644 index 0000000..63d00f6 --- /dev/null +++ b/src/Controller/User/Product/DuplicateProductAction.php @@ -0,0 +1,66 @@ + MyAccountAction::BASE_URL_EN.'/objects/{id}/duplicate', + 'fr' => MyAccountAction::BASE_URL_FR.'/objets/{id}/dupliquer', + ], + name: 'app_user_product_duplicate', + requirements: ['slug' => Requirement::ASCII_SLUG, 'id' => Requirement::UUID_V6] + )] + public function __invoke(string $id): Response + { + try { + /** @var Product $product */ + $product = $this->queryBus->query(new GetProductByIdQuery(Uuid::fromString($id), ProductVoter::DUPLICATE)); + } catch (HandlerFailedException $e) { + throw $e->getPrevious() instanceof HttpException ? $e->getPrevious() : $this->createNotFoundException($e->getMessage()); + } + + $command = new DuplicateProductCommand($product->getId(), ProductVoter::DUPLICATE); + /** @var Product $duplicatedProduct */ + $duplicatedProduct = $this->commandBus->dispatch($command); + $this->addFlashSuccess($this->getI18nPrefix().'.flash.success'); + + return $this->redirectToRoute('app_'.$duplicatedProduct->getType()->value.'_edit', ['id' => $duplicatedProduct->getId()]); + } +} diff --git a/src/Controller/User/Product/ObjectController.php b/src/Controller/User/Product/ObjectController.php new file mode 100644 index 0000000..db8b52d --- /dev/null +++ b/src/Controller/User/Product/ObjectController.php @@ -0,0 +1,89 @@ +createForm(ObjectFormType::class, $product)->handleRequest($request); + } + + #[Route(path: [ + 'en' => MyAccountAction::BASE_URL_EN.'/new-object', + 'fr' => MyAccountAction::BASE_URL_FR.'/nouvel-objet', + ], name: 'new')] + public function new(Request $request, #[CurrentUser] User $user): Response + { + $product = $this->productManager->initObject($user); + $form = $this->getForm($product, $request); + if ($form->isSubmitted() && $form->isValid()) { + /** @var array|null $images */ + $images = $form->get('images')->getData(); + $this->productManager->multipleUpload($images, $product); + $this->productManager->save($product, true); + + return $this->redirectToRoute('app_product_show', $product->getRoutingParameters()); + } + + return $this->render('pages/product/new_object.html.twig', compact('form')); + } + + #[Route(path: [ + 'en' => MyAccountAction::BASE_URL_EN.'/objects/{id}/edit', + 'fr' => MyAccountAction::BASE_URL_FR.'/objets/{id}/editer', + ], + name: 'edit', + requirements: ['slug' => Requirement::ASCII_SLUG, 'id' => Requirement::UUID_V6] + )] + public function edit(string $id, Request $request): Response + { + $product = $this->getProductForEdit($id); + $form = $this->getForm($product, $request); + if ($form->isSubmitted() && $form->isValid()) { + /** @var array|null $images */ + $images = $form->get('images')->getData(); + $this->productManager->multipleUpload($images, $product); + $this->productManager->save($product, true); + + return $this->redirectToRoute('app_product_show', $product->getRoutingParameters()); + } + + return $this->render('pages/product/edit_object.html.twig', compact('form', 'product')); + } +} diff --git a/src/Controller/User/Product/ProductAvailabilityController.php b/src/Controller/User/Product/ProductAvailabilityController.php new file mode 100644 index 0000000..857a08e --- /dev/null +++ b/src/Controller/User/Product/ProductAvailabilityController.php @@ -0,0 +1,76 @@ + MyAccountAction::BASE_URL_EN.'/my-products/{id}/availabilities', + 'fr' => MyAccountAction::BASE_URL_FR.'/mes-produits/{id}/disponibilites', + ], + name: self::ROUTE, + requirements: ['id' => Requirement::UUID_V6], + )] + public function __invoke(Request $request, string $id): Response + { + /** @var Product $product */ + $product = $this->queryBus->query(new GetProductByIdQuery(Uuid::fromString($id), ProductVoter::EDIT)); + + $unavailabilities = $this->queryBus->query(new GetProductUnavailabilitiesQuery($product->getId())); + + $form = $this->createForm(CreateProductAvailabilityType::class)->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + /** @var \DateTimeImmutable $startAt */ + $startAt = $form->get('startAt')->getData(); + /** @var \DateTimeImmutable $endAt */ + $endAt = $form->get('endAt')->getData(); + + $command = new CreateProductUnavailabilityCommand($product->getId(), $startAt, $endAt); + $this->commandBus->dispatch($command); + + $this->addFlashSuccess($this->getI18nPrefix().'.success'); + + return $this->redirectToRoute('app_product_show', ['id' => $id, 'slug' => $product->getSlug()]); + } + + return $this->render('pages/product/product_availability.html.twig', compact('product', 'id', 'form', 'unavailabilities')); + } +} diff --git a/src/Controller/User/Product/ProductTrait.php b/src/Controller/User/Product/ProductTrait.php new file mode 100644 index 0000000..0cbf30e --- /dev/null +++ b/src/Controller/User/Product/ProductTrait.php @@ -0,0 +1,27 @@ +queryBus->query(new GetProductByIdQuery(Uuid::fromString($id), ProductVoter::EDIT)); + } catch (HandlerFailedException $e) { + throw $e->getPrevious() instanceof HttpException ? $e->getPrevious() : $this->createNotFoundException($e->getMessage()); + } + + return $product; + } +} diff --git a/src/Controller/User/Product/ServiceController.php b/src/Controller/User/Product/ServiceController.php new file mode 100644 index 0000000..ba51c18 --- /dev/null +++ b/src/Controller/User/Product/ServiceController.php @@ -0,0 +1,90 @@ +createForm(ServiceFormType::class, $product)->handleRequest($request); + } + + #[Route(path: [ + 'en' => MyAccountAction::BASE_URL_EN.'/new-service', + 'fr' => MyAccountAction::BASE_URL_FR.'/nouveau-service', + ], name: 'new')] + public function new(Request $request, #[CurrentUser] User $user): Response + { + $product = $this->productManager->initService($user); + $form = $this->getForm($product, $request); + if ($form->isSubmitted() && $form->isValid()) { + /** @var array|null $images */ + $images = $form->get('images')->getData(); + $this->productManager->multipleUpload($images, $product); + $this->productManager->save($product, true); + + return $this->redirectToRoute('app_product_show', $product->getRoutingParameters()); + } + + return $this->render('pages/product/new_service.html.twig', compact('form')); + } + + #[Route([ + 'en' => MyAccountAction::BASE_URL_EN.'/services/{id}/edit', + 'fr' => MyAccountAction::BASE_URL_FR.'/services/{id}/editer', + ], + name: 'edit', + requirements: ['slug' => Requirement::ASCII_SLUG, 'id' => Requirement::UUID_V6] + )] + public function edit(string $id, Request $request): Response + { + $product = $this->getProductForEdit($id); + $form = $this->getForm($product, $request); + if ($form->isSubmitted() && $form->isValid()) { + /** @var array|null $images */ + $images = $form->get('images')->getData(); + $this->productManager->multipleUpload($images, $product); + $this->productManager->save($product, true); + + return $this->redirectToRoute('app_product_show', $product->getRoutingParameters()); + } + + return $this->render('pages/product/edit_service.html.twig', compact('form', 'product')); + } +} diff --git a/src/Controller/User/Product/UserProductsController.php b/src/Controller/User/Product/UserProductsController.php new file mode 100644 index 0000000..50d9a44 --- /dev/null +++ b/src/Controller/User/Product/UserProductsController.php @@ -0,0 +1,94 @@ + + * + * @return PaginationInterface + */ + private function paginate(Query $query, int $page): PaginationInterface + { + return $this->paginator->paginate($query, $page, self::MAX_ELEMENT_BY_PAGE); + } + + #[Route(path: [ + 'en' => MyAccountAction::BASE_URL_EN.'/my-objects', + 'fr' => MyAccountAction::BASE_URL_FR.'/mes-objets', + ], name: 'objects')] + public function userObjects(Request $request, #[CurrentUser] User $user): Response + { + $form = $this->createForm(ObjectCategorySelectFormType::class)->handleRequest($request); + $category = null; + if ($form->isSubmitted() && $form->isValid()) { + /** @var Category $category */ + $category = $form->get('category')->getData(); + } + + /** @var Query $query */ + $query = $this->queryBus->query(new GetUserObjectsQuery($user->getId(), $category?->getId())); + $pagination = $this->paginate($query, $this->getPage($request)); + + return $this->render('pages/account/product/list.html.twig', compact('pagination', 'form')); + } + + #[Route(path: [ + 'en' => MyAccountAction::BASE_URL_EN.'/my-services', + 'fr' => MyAccountAction::BASE_URL_FR.'/mes-services', + ], name: 'services')] + public function userServices(Request $request, #[CurrentUser] User $user): Response + { + $form = $this->createForm(ServiceCategorySelectFormType::class)->handleRequest($request); + $category = null; + if ($form->isSubmitted() && $form->isValid()) { + /** @var ?Category $category */ + $category = $form->get('category')->getData(); + } + /** @var Query $query */ + $query = $this->queryBus->query(new GetUserServicesQuery($user->getId(), $category?->getId())); + $pagination = $this->paginate($query, $this->getPage($request)); + + return $this->render('pages/account/product/list.html.twig', compact('pagination', 'form')); + } +} diff --git a/src/Controller/User/ServiceRequest/ConversationController.php b/src/Controller/User/ServiceRequest/ConversationController.php new file mode 100644 index 0000000..a399ca6 --- /dev/null +++ b/src/Controller/User/ServiceRequest/ConversationController.php @@ -0,0 +1,79 @@ + MyAccountAction::BASE_URL_EN.'/service/{id}/conversation', + 'fr' => MyAccountAction::BASE_URL_FR.'/service/{id}/conversation', + ], name: self::ROUTE, requirements: ['id' => Requirement::UUID_V6])] + public function __invoke(Request $request, string $id): Response + { + $serviceRequest = $this->getMyServiceRequest($id); + $this->commandBus->dispatch(new TryAutoFinalizeCommand($serviceRequest->getId())); + $this->commandBus->dispatch(new ReadMessagesCommand($serviceRequest->getId(), $this->getAppUser()->getId())); + + // we need to refresh the entity in case it was modified by the commands + $serviceRequest = $this->getMyServiceRequest($id); + + // form to add a new message + $form = $this->createForm(NewMessageType::class)->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + /** @var Message $message */ + $message = $form->getData(); + $this->commandBus->dispatch(new CreateMessageCommand($serviceRequest->getId(), $this->getAppUser()->getId(), $message->getMessage())); + $this->addFlashSuccess($this->getI18nPrefix().'.flash.success'); + + return $this->redirectToRoute(self::ROUTE, ['id' => $id]); + } + + // form to modify the dates of the service request + $modifyForm = $this->createForm(ModifyServiceRequestType::class, $serviceRequest)->handleRequest($request); + + return $this->render('pages/account/conversation.html.twig', [ + 'service_request' => $serviceRequest, + 'form' => $form, + 'modify_form' => $modifyForm, + ]); + } +} diff --git a/src/Controller/User/ServiceRequest/MyLendingsAction.php b/src/Controller/User/ServiceRequest/MyLendingsAction.php new file mode 100644 index 0000000..be2cfef --- /dev/null +++ b/src/Controller/User/ServiceRequest/MyLendingsAction.php @@ -0,0 +1,57 @@ + MyAccountAction::BASE_URL_EN.'/my-lendings', + 'fr' => MyAccountAction::BASE_URL_FR.'/mes-prets', + ], name: 'app_user_my_lendings')] + public function __invoke(Request $request, #[CurrentUser] User $user): Response + { + $form = $this->createForm(UserLendingProductSelectFormType::class)->handleRequest($request); + + /** @var ?ArrayCollection $selectedProducts */ + $selectedProducts = $form->get('product')->getData(); + + $query = $this->queryBus->query(new GetLendingsQuery($user->getId(), $selectedProducts)); + $pagination = $this->paginator->paginate($query, $this->getPage($request), self::MAX_ELEMENT_BY_PAGE); + + return $this->render('pages/account/lendings/list.html.twig', compact('pagination', 'form', 'selectedProducts')); + } +} diff --git a/src/Controller/User/ServiceRequest/MyLoansAction.php b/src/Controller/User/ServiceRequest/MyLoansAction.php new file mode 100644 index 0000000..967ff9e --- /dev/null +++ b/src/Controller/User/ServiceRequest/MyLoansAction.php @@ -0,0 +1,55 @@ + MyAccountAction::BASE_URL_EN.'/my-loans', + 'fr' => MyAccountAction::BASE_URL_FR.'/mes-emprunts', + ], name: 'app_user_my_loans')] + public function __invoke(Request $request, #[CurrentUser] User $user): Response + { + $form = $this->createForm(UserLoansProductSelectFormType::class)->handleRequest($request); + + /** @var ?ArrayCollection $selectedProducts */ + $selectedProducts = $form->get('product')->getData(); + + $query = $this->queryBus->query(new GetLoansQuery($user->getId(), $selectedProducts)); + $pagination = $this->paginator->paginate($query, $this->getPage($request), self::MAX_ELEMENT_BY_PAGE); + + return $this->render('pages/account/loans/list.html.twig', compact('pagination', 'form')); + } +} diff --git a/src/Controller/User/ServiceRequest/ServiceRequestController.php b/src/Controller/User/ServiceRequest/ServiceRequestController.php new file mode 100644 index 0000000..bd835b5 --- /dev/null +++ b/src/Controller/User/ServiceRequest/ServiceRequestController.php @@ -0,0 +1,75 @@ + MyAccountAction::BASE_URL_EN.'/new-service-request/{id}', + 'fr' => MyAccountAction::BASE_URL_FR.'/nouvelle-demande-de-service/{id}', + ], name: self::ROUTE, requirements: ['id' => Requirement::UUID_V6])] + public function __invoke(Request $request, string $id, #[CurrentUser] User $user): Response + { + try { + /** @var Product $product */ + $product = $this->queryBus->query(new GetProductByIdQuery(Uuid::fromString($id))); + } catch (HandlerFailedException $e) { + throw $this->createNotFoundException($e->getMessage()); + } + + $serviceRequest = $this->serviceRequestManager->initFormProductAndRequest($product, $request); + $form = $this->createForm(CreateServiceRequestType::class, $serviceRequest)->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + /** @var ServiceRequest $newSr */ + $newSr = $form->getData(); + $command = new CreateServiceRequestCommand($product->getId(), $user->getId(), $newSr->getStartAt(), $newSr->getEndAt(), $newSr->getMessage()); + /** @var ServiceRequest $serviceRequest */ + $serviceRequest = $this->commandBus->dispatch($command); + $this->addFlashSuccess('loan.new_action.form.success'); + + return $this->redirectToRoute(ConversationController::ROUTE, ['id' => (string) $serviceRequest->getId()]); + } + + return $this->render('pages/account/loans/new.html.twig', compact('form', 'product')); + } +} diff --git a/src/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowController.php b/src/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowController.php new file mode 100644 index 0000000..7d44e1f --- /dev/null +++ b/src/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowController.php @@ -0,0 +1,118 @@ + Requirement::UUID_V6, + 'transition' => new EnumRequirement(ServiceRequestStatusTransition::class), + ], + methods: ['POST'], + )] + public function apply(Request $request, string $id, ServiceRequestStatusTransition $transition): Response + { + /** @var ?string $submittedToken */ + $submittedToken = $request->request->get('token'); + if (!$this->isCsrfTokenValid('transition', $submittedToken)) { + throw new UnprocessableEntityHttpException('Invalid CSRF token'); + } + + $serviceRequest = $this->getMyServiceRequest($id); + + return $this->applyAndRedirect($id, $serviceRequest, $transition); + } + + /** + * Specific controller for transitions having to pass extra information. + */ + #[Route( + path: MyAccountAction::BASE_URL_FR.'/service/{id}/transition/modify/{transition}', + name: self::ROUTE.'_modify', + requirements: [ + 'id' => Requirement::UUID_V6, + 'transition' => new EnumRequirement([ + ServiceRequestStatusTransition::MODIFY_OWNER, + ServiceRequestStatusTransition::MODIFY_RECIPIENT, + ]), + ], + methods: ['POST'] + )] + public function applyModifyOwner(Request $request, string $id, ServiceRequestStatusTransition $transition): Response + { + $serviceRequest = $this->getMyServiceRequest($id); + $form = $this->createForm(ModifyServiceRequestType::class, $serviceRequest)->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $this->doctrine->flush(); // entity is modified at form submission + + return $this->applyAndRedirect($id, $serviceRequest, $transition); + } + + return $this->forward(ConversationController::class, compact('id')); + } + + private function applyAndRedirect(string $id, ServiceRequest $serviceRequest, ServiceRequestStatusTransition $transition): Response + { + try { + $this->serviceRequestStatusWorkflow->apply($serviceRequest, $transition); + } catch (\LogicException $e) { + throw new UnprocessableEntityHttpException($e->getMessage(), $e); + } + $this->doctrine->flush(); + $this->addFlashSuccess($this->getI18nPrefix().'.flash.'.$serviceRequest->getProduct()->getType()->value.'.'.u($transition->value)->snake()); + + return $this->redirectToRoute(ConversationController::ROUTE, compact('id')); + } +} diff --git a/src/Controller/User/ServiceRequest/ServiceRequestTrait.php b/src/Controller/User/ServiceRequest/ServiceRequestTrait.php new file mode 100644 index 0000000..2af5b99 --- /dev/null +++ b/src/Controller/User/ServiceRequest/ServiceRequestTrait.php @@ -0,0 +1,37 @@ +queryBus->query(new GetServiceRequestByIdQuery(Uuid::fromString($id))); + } catch (HandlerFailedException $e) { + /** @var \Exception $previous */ + $previous = $e->getPrevious(); + throw match (\get_class($previous)) { + AccessDeniedException::class => $this->createAccessDeniedException($previous->getMessage()), + default => $this->createNotFoundException($previous->getMessage()), + }; + } + + return $serviceRequest; + } +} diff --git a/src/Controller/User/VacationModeAction.php b/src/Controller/User/VacationModeAction.php new file mode 100644 index 0000000..38e83aa --- /dev/null +++ b/src/Controller/User/VacationModeAction.php @@ -0,0 +1,43 @@ + MyAccountAction::BASE_URL_EN.'/vacation-mode', + 'fr' => MyAccountAction::BASE_URL_FR.'/mode-vacances', + ], name: 'user_toggle_vacation_mode')] + public function __invoke(#[CurrentUser] User $user, string $_route): Response + { + $command = new ChangeVacationModeCommand($user->getId()); + $this->commandBus->dispatch($command); + $this->addFlashSuccess($this->getI18nPrefix().'.flash.success'); + + return $this->redirectToRoute(MyAccountAction::ROUTE); + } +} diff --git a/src/Controller/i18nTrait.php b/src/Controller/i18nTrait.php new file mode 100644 index 0000000..eb63463 --- /dev/null +++ b/src/Controller/i18nTrait.php @@ -0,0 +1,30 @@ +split('\\'); + + // apply snake case on each entry (which also applies lower) + $hierarchy = array_map(static fn (UnicodeString $string) => $string->snake()->toString(), $hierarchy); + + // then join the folders with a dot + return implode('.', $hierarchy); + } +} diff --git a/src/DataFixtures/Processor/ValidationProcessor.php b/src/DataFixtures/Processor/ValidationProcessor.php new file mode 100644 index 0000000..b1c2f32 --- /dev/null +++ b/src/DataFixtures/Processor/ValidationProcessor.php @@ -0,0 +1,31 @@ +validator->validate($object); + if ($violations->count() > 0) { + $message = sprintf("Error when validating fixture \"%s\", violation(s) detected:\n%s", $id, $violations); + throw new \DomainException($message); + } + } + + public function postProcess(string $id, object $object): void + { + } +} diff --git a/src/DependencyInjection/LocalesCompilerPass.php b/src/DependencyInjection/LocalesCompilerPass.php new file mode 100644 index 0000000..10c9ce1 --- /dev/null +++ b/src/DependencyInjection/LocalesCompilerPass.php @@ -0,0 +1,23 @@ +getParameter('kernel.enabled_locales'); + $container->setParameter('requirements_locales', implode('|', $enabledLocales)); + } +} diff --git a/src/Doctrine/Behavior/TimestampableEntity.php b/src/Doctrine/Behavior/TimestampableEntity.php new file mode 100644 index 0000000..46e9e9d --- /dev/null +++ b/src/Doctrine/Behavior/TimestampableEntity.php @@ -0,0 +1,49 @@ +createdAt = $createdAt; + + return $this; + } + + public function getCreatedAt(): \DateTimeImmutable + { + return $this->createdAt; + } + + public function setUpdatedAt(\DateTimeImmutable $updatedAt): static + { + $this->updatedAt = $updatedAt; + + return $this; + } + + public function getUpdatedAt(): \DateTimeImmutable + { + return $this->updatedAt; + } +} diff --git a/src/Doctrine/Listener/ProductListener.php b/src/Doctrine/Listener/ProductListener.php new file mode 100644 index 0000000..02188a5 --- /dev/null +++ b/src/Doctrine/Listener/ProductListener.php @@ -0,0 +1,43 @@ +isIndexable()) { + $this->meilisearch->indexProduct($product); + } else { + // remove from index + $this->meilisearch->deleteProduct($product); + } + } + + public function postPersist(Product $product): void + { + $this->meilisearch->indexProduct($product); + } + + public function preRemove(Product $product): void + { + $this->meilisearch->deleteProduct($product); + } +} diff --git a/src/Doctrine/Listener/UserListener.php b/src/Doctrine/Listener/UserListener.php new file mode 100644 index 0000000..890f868 --- /dev/null +++ b/src/Doctrine/Listener/UserListener.php @@ -0,0 +1,65 @@ +getPlainPassword()); + if ($plainPassword->isEmpty()) { + return; + } + + $this->userManager->updatePassword($user); + } + + /** + * Normalization stuff. + */ + public function preUpdate(User $user): void + { + $this->normalize($user); + } + + /** + * Normalization stuff. + */ + public function prePersist(User $user): void + { + $this->normalize($user); + } + + /** + * Common normalize method. + */ + private function normalize(User $user): void + { + $this->userManager->normalizeEmail($user, $user->getEmail()); + } +} diff --git a/src/Doctrine/Manager/MessageManager.php b/src/Doctrine/Manager/MessageManager.php new file mode 100644 index 0000000..aa93961 --- /dev/null +++ b/src/Doctrine/Manager/MessageManager.php @@ -0,0 +1,88 @@ +messageRepository->save($entity, $flush); + } + + /** + * @param array $parameters + */ + public function getSystemMessage(string $id, array $parameters): string + { + return $this->translator->trans($this->getI18nPrefix().'.'.$id, $parameters, self::DOMAIN); + } + + /** + * @param array $messageParameters + */ + public function createSystemMessage( + ServiceRequest $serviceRequest, + string $messageTemplate, + array $messageParameters = [], + \DateTimeImmutable $createdAt = null, + ): Message { + $message = (new Message()) + ->setServiceRequest($serviceRequest) + ->setType(MessageType::SYSTEM) + ->setMessageTemplate($messageTemplate) + ->setMessageParameters($messageParameters) + ->setMessage($this->getSystemMessage($messageTemplate, $messageParameters)); + + // allow to force Timestampable dates + if ($createdAt !== null) { + $message->setCreatedAt($createdAt) + ->setUpdatedAt($createdAt); + } + + return $message; + } + + public function createFromRecipientMessage(ServiceRequest $serviceRequest, string $message, \DateTimeImmutable $createdAt = null): Message + { + $message = (new Message()) + ->setServiceRequest($serviceRequest) + ->setType(MessageType::FROM_RECIPIENT) + ->setMessage($message) + ; + + // allow to force Timestampable dates + if ($createdAt !== null) { + $message->setCreatedAt($createdAt) + ->setUpdatedAt($createdAt); + } + + return $message; + } + + public function createFromOwnerMessage(ServiceRequest $serviceRequest, string $message): Message + { + return (new Message()) + ->setServiceRequest($serviceRequest) + ->setType(MessageType::FROM_OWNER) + ->setMessage($message); + } +} diff --git a/src/Doctrine/Manager/ProductAvailabilityManager.php b/src/Doctrine/Manager/ProductAvailabilityManager.php new file mode 100644 index 0000000..b112b09 --- /dev/null +++ b/src/Doctrine/Manager/ProductAvailabilityManager.php @@ -0,0 +1,38 @@ +productAvailabilityRepository->save($entity, $flush); + } + + /** + * Create the unavailability of a product for a given service request. + */ + public function createFromServiceRequest(ServiceRequest $serviceRequest, \DateTimeImmutable $startAt, \DateTimeImmutable $endAt): ProductAvailability + { + return (new ProductAvailability()) + ->setMode(ProductAvailabilityMode::UNAVAILABLE) + ->setType(ProductAvailabilityType::SERVICE_REQUEST) + ->setServiceRequest($serviceRequest) + ->setProduct($serviceRequest->getProduct()) + ->setStartAt($startAt) + ->setEndAt($endAt); + } +} diff --git a/src/Doctrine/Manager/ProductManager.php b/src/Doctrine/Manager/ProductManager.php new file mode 100644 index 0000000..504115c --- /dev/null +++ b/src/Doctrine/Manager/ProductManager.php @@ -0,0 +1,123 @@ +productRepository->save($entity, $flush); + } + + /** + * Duplicate product and handle translations. + */ + public function duplicate(Product $product): Product + { + $duplicated = $product->duplicate(); + $duplicated->setName($this->translator->trans($this->getI18nPrefix().'.duplicate.copy_of').$product->getName()); + + return $duplicated; + } + + /** + * @param array|null $images + */ + public function multipleUpload(?array $images, Product $newProduct): void + { + if ($images !== null && \count($images) !== 0) { + $imagesUploaded = $this->fileUploader->uploadImageArray($this->productStorage, $images); + $newProduct->addImages($imagesUploaded); + } + } + + /** + * Delete a photo in the db and in the storage. + */ + public function deleteImage(Product $product, string $image): Product + { + try { + $this->productStorage->delete($image); + } catch (FilesystemException $e) { + $this->logger->warning(sprintf('Unable to delete product (%s) image %s: %s', $product->getId(), $image, $e->getMessage())); + } + + $product->deleteImage($image); + $this->save($product, true); + + return $product; + } + + public function hasProductsOnlyInGroup(Group $group, ?User $user): bool + { + // not logged + if ($user === null) { + return false; + } + + // check the products published in this specific group + $productsQuery = $this->productRepository->getUserProductsByType($user, null, null, $group); + /** @var array $products */ + $products = $productsQuery->execute(); + + // no product, so nothing to check + if (\count($products) === 0) { + return false; + } + + // now check if those products are published in other groups than this one + // as we already know the product is published in at least one group, we + // just have to check that the count in greater than 1 (the group + at least + // another one) + foreach ($products as $product) { + if ($product->getGroups()->count() > 1) { + return false; + } + } + + return true; + } + + public function initObject(User $user): Product + { + return (new Product()) + ->setOwner($user) + ->setType(ProductType::OBJECT) + ->setStatus(ProductStatus::ACTIVE); + } + + public function initService(User $user): Product + { + return (new Product()) + ->setOwner($user) + ->setType(ProductType::SERVICE) + ->setStatus(ProductStatus::ACTIVE); + } +} diff --git a/src/Doctrine/Manager/ServiceRequestManager.php b/src/Doctrine/Manager/ServiceRequestManager.php new file mode 100644 index 0000000..cdbe982 --- /dev/null +++ b/src/Doctrine/Manager/ServiceRequestManager.php @@ -0,0 +1,94 @@ +serviceRequestRepository->save($entity, $flush); + } + + public function readMessages(ServiceRequest $serviceRequest, User $user): void + { + foreach ($serviceRequest->getMessages() as $message) { + if ($serviceRequest->isOwner($user)) { + $message->setOwnerRead(true); + $message->setOwnerReadAt(new \DateTimeImmutable('now')); + } else { + $message->setRecipientRead(true); + $message->setRecipientReadAt(new \DateTimeImmutable('now')); + } + } + + $this->save($serviceRequest, true); + } + + /** + * Initialize a new service request for a product with sensible default values. + */ + public function initFormProductAndRequest(Product $product, Request $request): ServiceRequest + { + $serviceRequest = (new ServiceRequest()) + ->setMessage($this->translator->trans( + self::TRANS_PREFIX.'.message.'.$product->getType()->value.'.default', + ['%product%' => u($product->getName())->lower()->toString()], + )) + ->setProduct($product) + ; + + $startAt = $request->query->getAlnum('startAt'); + if (!u($startAt)->isEmpty()) { + try { + $serviceRequest->setStartAt(new \DateTimeImmutable($startAt)); + } catch (\Exception) { + } + } + $endAt = $request->query->getAlnum('endAt'); + if (!u($endAt)->isEmpty()) { + try { + $serviceRequest->setEndAt(new \DateTimeImmutable($endAt)); + } catch (\Exception) { + } + } + + return $serviceRequest; + } + + /** + * Delete all unavailabilities linked to the service request. + */ + public function deleteUnavailabilities(ServiceRequest $serviceRequest): void + { + $toDelete = $serviceRequest->getProduct()->getAvailabilities()->filter( + fn (ProductAvailability $productAvailability) => $productAvailability->getServiceRequest() === $serviceRequest + ); + foreach ($toDelete as $item) { + $this->entityManager->remove($item); + } + $this->entityManager->flush(); + } +} diff --git a/src/Doctrine/Manager/UserManager.php b/src/Doctrine/Manager/UserManager.php new file mode 100644 index 0000000..ee8d0ad --- /dev/null +++ b/src/Doctrine/Manager/UserManager.php @@ -0,0 +1,210 @@ +userRepository->save($entity, $flush); + } + + /** + * Shortcut. + */ + public function remove(User $entity, bool $flush = false): void + { + $this->userRepository->remove($entity, $flush); + } + + public function updatePassword(User $user): void + { + Assert::stringNotEmpty((string) $user->getPlainPassword(), 'The plainPassword property should be set and not empty.'); + $password = $this->userPasswordHasher->hashPassword($user, (string) $user->getPlainPassword()); + $user->setPassword($password); + } + + public function updateLoginAt(User $user): void + { + $user->setLoginAt($this->clock->now()); + $this->save($user, true); + } + + /** + * Normalize email in a reliable way. + */ + public function normalizeEmail(User $user, string $email): void + { + $user->setEmail($this->stringHelper->normalizeEmail($email)); + } + + /** + * Generate a random token that will be used for the user to confirm its email. + */ + public function generateConfirmationToken(User $user): void + { + $confirmationToken = ByteString::fromRandom(self::CONFIRMATION_TOKEN_LENGTH); + $user->setConfirmationToken($confirmationToken->toString()); + } + + /** + * Generate a random token that will be used for the user to confirm its email. + */ + public function generateLostPasswordToken(User $user): void + { + $token = ByteString::fromRandom(self::LOST_PASSWORD_TOKEN_LENGTH); + $user->setLostPasswordToken($token->toString()); + } + + /** + * Set the expiration date of the confirmation token. + */ + public function setConfirmationTokenExpirationDate(User $user): void + { + $expiresAt = $this->clock->now()->modify(self::CONFIRMATION_TOKEN_EXPIRATION_TIME); + $user->setConfirmationExpiresAt($expiresAt); + } + + /** + * Set the expiration date of the lost password token. + */ + public function setLostPasswordExpirationDate(User $user): void + { + $expiresAt = $this->clock->now()->modify(self::LOST_PASSWORD_TOKEN_EXPIRATION_TIME); + $user->setLostPasswordExpiresAt($expiresAt); + } + + public function refreshConfirmationToken(User $user): void + { + $this->generateConfirmationToken($user); + $this->setConfirmationTokenExpirationDate($user); + } + + public function getStep1User(string $email): User + { + $user = new User(); + $user->setEmail($email); + $this->refreshConfirmationToken($user); + + return $user; + } + + public function refreshLostPasswordToken(User $user): void + { + $this->generateLostPasswordToken($user); + $this->setLostPasswordExpirationDate($user); + } + + /** + * Finalization process and cleanup for step 2 of the account creation. + */ + public function finalizeAccountCreateStep2(User $user): void + { + $user->confirmEmail(); + $user->resetConfirmation(); + } + + /** + * Set the user's new email. + */ + public function changeLogin(User $user, string $email): void + { + $user->setEmail($email); + } + + /** + * Add a membership for a free group. + */ + public function addToGroup(User $user, Group $group, UserMembership $userMembership = UserMembership::MEMBER): void + { + $userGroup = (new UserGroup()) + ->setUser($user) + ->setGroup($group) + ->setMembership($userMembership); + $user->addUserGroup($userGroup); + } + + public function addInvitation(User $user, Group $group): void + { + $this->addToGroup($user, $group, UserMembership::INVITATION); + } + + public function upload(?UploadedFile $image, User $user): void + { + if ($image !== null) { + $imageUploaded = $this->fileUploader->uploadImage($this->userStorage, $image); + $user->setAvatar($imageUploaded); + } + } + + public function deleteAvatar(User $user): User + { + try { + $this->userStorage->delete((string) $user->getAvatar()); + } catch (FilesystemException $e) { + $this->logger->warning(sprintf('Unable to avatar of user (%s) image %s: %s', $user->getId(), $user->getAvatar(), $e->getMessage())); + } + $user->deleteAvatar(); + $this->save($user, true); + + return $user; + } + + /** + * Add the email normalization step when submitting a form implying a user so + * the unique constraint on the email can work properly. + */ + public function addEmailNormalizeSubmitEvent(FormBuilderInterface $builder): void + { + $builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) { + /** @var User $user */ + $user = $event->getData(); + $this->normalizeEmail($user, $user->getEmail()); + $event->setData($user); + }); + } +} diff --git a/src/Dto/Product/Search.php b/src/Dto/Product/Search.php new file mode 100644 index 0000000..c570464 --- /dev/null +++ b/src/Dto/Product/Search.php @@ -0,0 +1,94 @@ +q = $q; + $this->page = $page; + $this->user = $user; + } + + /** + * Search query. Eg: "vélo". + */ + public string $q = ''; + + /** + * Requested page for paginated results. + */ + public int $page = 1; + + /** + * Category filter. + */ + public ?Category $category = null; + + /** + * Place filter. + */ + public ?User $place = null; + + /** + * Current logged user. + */ + public ?User $user = null; + + /** + * City filter Eg: "Lille". The distance filter is only applied when we have + * bother a city and a distance. + */ + public ?Address $city = null; + + /** + * Distance filter related to the city. + */ + public ?int $distance = null; + + public function hasQuery(): bool + { + return $this->q !== ''; + } + + public function hasCity(): bool + { + return $this->city !== null; + } + + public function hasDistance(): bool + { + return $this->distance !== null; + } + + /** + * Test if we have both a city (as an address with a non empty locality) and + * a distance. + */ + public function hasProximity(): bool + { + return $this->hasCity() && + ($this->city?->hasLocality() ?? false) && + $this->hasDistance() + ; + } + + /** + * If the user is not null, then it is logged. + */ + public function isLogged(): bool + { + return $this->user !== null; + } +} diff --git a/src/Dto/User/UserAddressStep1Data.php b/src/Dto/User/UserAddressStep1Data.php new file mode 100644 index 0000000..98715ab --- /dev/null +++ b/src/Dto/User/UserAddressStep1Data.php @@ -0,0 +1,58 @@ + $this->address, + 'addresses' => $this->addresses, + ]; + } + + /** + * @return array + * + * @throws \Exception + */ + public function getAddressesAsArray(): array + { + /** @var array $array */ + $array = iterator_to_array($this->addresses->getIterator()); + + return $array; + } +} diff --git a/src/EasyAdmin/Field/FieldTrait.php b/src/EasyAdmin/Field/FieldTrait.php new file mode 100644 index 0000000..d84fe46 --- /dev/null +++ b/src/EasyAdmin/Field/FieldTrait.php @@ -0,0 +1,35 @@ +renderAsSwitch(false) + ->setTemplatePath('easy_admin/field/boolean.html.twig'); + } + + /** + * @return array + */ + public function getPanels(): array + { + return [ + 'information' => FormField::addPanel('panel.information', 'fas fa-info-circle'), + 'tech_information' => FormField::addPanel('panel.tech_information', 'fas fa-history'), + ]; + } +} diff --git a/src/EasyAdmin/Filter/EnumFilter.php b/src/EasyAdmin/Filter/EnumFilter.php new file mode 100644 index 0000000..7ade70f --- /dev/null +++ b/src/EasyAdmin/Filter/EnumFilter.php @@ -0,0 +1,38 @@ +setFilterFqcn(__CLASS__) + ->setProperty($propertyName) + ->setLabel($label) + ->setFormType($formType); + } + + /** + * Applie an exact match filter for an enum value. + */ + public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto, ?FieldDto $fieldDto, EntityDto $entityDto): void + { + $queryBuilder->andWhere(sprintf('%s.%s = :value', $filterDataDto->getEntityAlias(), $filterDataDto->getProperty())) + ->setParameter('value', $filterDataDto->getValue()); + } +} diff --git a/src/EasyAdmin/Filter/User/GroupFilter.php b/src/EasyAdmin/Filter/User/GroupFilter.php new file mode 100644 index 0000000..92fe638 --- /dev/null +++ b/src/EasyAdmin/Filter/User/GroupFilter.php @@ -0,0 +1,45 @@ +setFilterFqcn(__CLASS__) + ->setProperty($propertyName) + ->setLabel($label) + ->setFormType(GroupType::class); + } + + /** + * The join is done on userGroups. + */ + public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto, ?FieldDto $fieldDto, EntityDto $entityDto): void + { + /** @var Group $group */ + $group = $filterDataDto->getValue(); + $queryBuilder + ->innerJoin(sprintf('%s.userGroups', $filterDataDto->getEntityAlias()), 'ug') + ->andWhere('ug.group = :group') + ->setParameter(':group', $group->getId()) + ; + } +} diff --git a/src/EasyAdmin/Filter/User/MyUsersFilter.php b/src/EasyAdmin/Filter/User/MyUsersFilter.php new file mode 100644 index 0000000..4b2b8de --- /dev/null +++ b/src/EasyAdmin/Filter/User/MyUsersFilter.php @@ -0,0 +1,42 @@ +setFilterFqcn(__CLASS__) + ->setProperty($propertyName) + ->setLabel($label) + ->setFormType(UserType::class) + ; + } + + public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto, ?FieldDto $fieldDto, EntityDto $entityDto): void + { + /** @var User $user */ + $user = $filterDataDto->getValue(); + $queryBuilder + ->andWhere('entity.user = :user') + ->setParameter(':user', $user) + ; + } +} diff --git a/src/EasyAdmin/Filter/UserGroup/MyGroupFilter.php b/src/EasyAdmin/Filter/UserGroup/MyGroupFilter.php new file mode 100644 index 0000000..4ccd28c --- /dev/null +++ b/src/EasyAdmin/Filter/UserGroup/MyGroupFilter.php @@ -0,0 +1,42 @@ +setFilterFqcn(__CLASS__) + ->setProperty($propertyName) + ->setLabel($label) + ->setFormType(GroupType::class) + ; + } + + public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto, ?FieldDto $fieldDto, EntityDto $entityDto): void + { + /** @var Group $group */ + $group = $filterDataDto->getValue(); + $queryBuilder + ->andWhere('entity.group = :group') + ->setParameter(':group', $group->getId()) + ; + } +} diff --git a/src/EasyAdmin/Filter/UuidFilter.php b/src/EasyAdmin/Filter/UuidFilter.php new file mode 100644 index 0000000..6a96a43 --- /dev/null +++ b/src/EasyAdmin/Filter/UuidFilter.php @@ -0,0 +1,43 @@ +add(UuidFilter::new('id', TextType::class)) + */ +final class UuidFilter implements FilterInterface +{ + use FilterTrait; + + public static function new(string $propertyName, string $label = null): self + { + return (new self()) + ->setFilterFqcn(__CLASS__) + ->setProperty($propertyName) + ->setLabel($label) + ->setFormType(TextType::class); + } + + /** + * Apply an exact match filter for the uuid. Doctrine can handle the value as + * a string and we don't have to convert it in a uuid object. + */ + public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto, ?FieldDto $fieldDto, EntityDto $entityDto): void + { + $queryBuilder->andWhere(sprintf('%s.%s = :value', $filterDataDto->getEntityAlias(), $filterDataDto->getProperty())) + ->setParameter('value', $filterDataDto->getValue()); + } +} diff --git a/src/EasyAdmin/Form/Type/GroupMembershipType.php b/src/EasyAdmin/Form/Type/GroupMembershipType.php new file mode 100644 index 0000000..343f6e3 --- /dev/null +++ b/src/EasyAdmin/Form/Type/GroupMembershipType.php @@ -0,0 +1,30 @@ +setDefaults([ + 'choices' => GroupMembership::getAsArray(), + 'translation_domain' => DashboardController::DOMAIN, + ]); + } + + public function getParent(): string + { + return ChoiceType::class; + } +} diff --git a/src/EasyAdmin/Form/Type/GroupOfferTypeType.php b/src/EasyAdmin/Form/Type/GroupOfferTypeType.php new file mode 100644 index 0000000..719e9fb --- /dev/null +++ b/src/EasyAdmin/Form/Type/GroupOfferTypeType.php @@ -0,0 +1,30 @@ +setDefaults([ + 'choices' => GroupOfferType::getAsArray(), + 'translation_domain' => DashboardController::DOMAIN, + ]); + } + + public function getParent(): string + { + return ChoiceType::class; + } +} diff --git a/src/EasyAdmin/Form/Type/GroupType.php b/src/EasyAdmin/Form/Type/GroupType.php new file mode 100644 index 0000000..e49c8e3 --- /dev/null +++ b/src/EasyAdmin/Form/Type/GroupType.php @@ -0,0 +1,52 @@ +setDefaults([ + 'class' => Group::class, + 'className' => Group::class, + 'translation_domain' => DashboardController::DOMAIN, + ]); + + // restrict to allowed groups only + if (!$this->authorizationChecker->isAdmin()) { + /** @var User $user */ + $user = $this->security->getUser(); + $resolver->setDefault('query_builder', function (GroupRepository $repo) use ($user) { + return $repo->createQueryBuilder('entity') + ->andWhere('entity.id IN (:groups)') + ->setParameter(':groups', $user->getMyGroupsAsAdmin()); + }); + } + } + + public function getParent(): string + { + return EntityType::class; + } +} diff --git a/src/EasyAdmin/Form/Type/GroupTypeType.php b/src/EasyAdmin/Form/Type/GroupTypeType.php new file mode 100644 index 0000000..462e9df --- /dev/null +++ b/src/EasyAdmin/Form/Type/GroupTypeType.php @@ -0,0 +1,30 @@ +setDefaults([ + 'choices' => GroupType::getAsArray(), + 'translation_domain' => DashboardController::DOMAIN, + ]); + } + + public function getParent(): string + { + return ChoiceType::class; + } +} diff --git a/src/EasyAdmin/Form/Type/LoanStatusType.php b/src/EasyAdmin/Form/Type/LoanStatusType.php new file mode 100644 index 0000000..095015b --- /dev/null +++ b/src/EasyAdmin/Form/Type/LoanStatusType.php @@ -0,0 +1,27 @@ +setDefaults([ + 'choices' => ServiceRequestStatus::getAsArray(), + 'translation_domain' => DashboardController::DOMAIN, + ]); + } + + public function getParent(): string + { + return ChoiceType::class; + } +} diff --git a/src/EasyAdmin/Form/Type/ProductStatusType.php b/src/EasyAdmin/Form/Type/ProductStatusType.php new file mode 100644 index 0000000..65fb755 --- /dev/null +++ b/src/EasyAdmin/Form/Type/ProductStatusType.php @@ -0,0 +1,30 @@ +setDefaults([ + 'choices' => ProductStatus::getAsArray(), + 'translation_domain' => DashboardController::DOMAIN, + ]); + } + + public function getParent(): string + { + return ChoiceType::class; + } +} diff --git a/src/EasyAdmin/Form/Type/ProductVisibilityType.php b/src/EasyAdmin/Form/Type/ProductVisibilityType.php new file mode 100644 index 0000000..5b991d6 --- /dev/null +++ b/src/EasyAdmin/Form/Type/ProductVisibilityType.php @@ -0,0 +1,30 @@ +setDefaults([ + 'choices' => ProductVisibility::getAsArray(), + 'translation_domain' => DashboardController::DOMAIN, + ]); + } + + public function getParent(): string + { + return ChoiceType::class; + } +} diff --git a/src/EasyAdmin/Form/Type/SocialMediaTypeType.php b/src/EasyAdmin/Form/Type/SocialMediaTypeType.php new file mode 100644 index 0000000..9aeee21 --- /dev/null +++ b/src/EasyAdmin/Form/Type/SocialMediaTypeType.php @@ -0,0 +1,27 @@ +setDefaults([ + 'choices' => SocialMediaType::getAsArray(), + 'translation_domain' => DashboardController::DOMAIN, + ]); + } + + public function getParent(): string + { + return ChoiceType::class; + } +} diff --git a/src/EasyAdmin/Form/Type/UserMembershipType.php b/src/EasyAdmin/Form/Type/UserMembershipType.php new file mode 100644 index 0000000..000817b --- /dev/null +++ b/src/EasyAdmin/Form/Type/UserMembershipType.php @@ -0,0 +1,30 @@ +setDefaults([ + 'choices' => UserMembership::getAsArray(), + 'translation_domain' => DashboardController::DOMAIN, + ]); + } + + public function getParent(): string + { + return ChoiceType::class; + } +} diff --git a/src/EasyAdmin/Form/Type/UserType.php b/src/EasyAdmin/Form/Type/UserType.php new file mode 100644 index 0000000..4389e64 --- /dev/null +++ b/src/EasyAdmin/Form/Type/UserType.php @@ -0,0 +1,53 @@ +setDefaults([ + 'class' => User::class, + 'className' => User::class, + 'translation_domain' => DashboardController::DOMAIN, + ]); + + // restrict to allowed groups only + if (!$this->authorizationChecker->isAdmin()) { + /** @var User $user */ + $user = $this->security->getUser(); + + $resolver->setDefault('query_builder', function (UserRepository $repo) use ($user) { + return $repo->createQueryBuilder('entity') + ->innerJoin('entity.userGroups', 'ug') + ->andWhere('ug.group IN (:groups)') + ->setParameter(':groups', $user->getMyGroupsAsAdmin()); + }); + } + } + + public function getParent(): string + { + return EntityType::class; + } +} diff --git a/src/Entity/.gitignore b/src/Entity/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/Entity/Address.php b/src/Entity/Address.php new file mode 100644 index 0000000..262aebe --- /dev/null +++ b/src/Entity/Address.php @@ -0,0 +1,389 @@ +displayName; + } + + public function getId(): Uuid + { + return $this->id; + } + + public function setId(Uuid $id): self + { + $this->id = $id; + + return $this; + } + + public function getAddress(): string + { + return $this->address; + } + + public function setAddress(string $address): self + { + $this->address = $address; + + return $this; + } + + public function getAddressSupplement(): ?string + { + return $this->addressSupplement; + } + + public function setAddressSupplement(?string $addressSupplement): Address + { + $this->addressSupplement = $addressSupplement; + + return $this; + } + + public function getDisplayName(): ?string + { + return $this->displayName; + } + + public function setDisplayName(?string $displayName): self + { + $this->displayName = $displayName; + + return $this; + } + + public function getStreetNumber(): string + { + return $this->streetNumber; + } + + public function setStreetNumber(string $streetNumber): self + { + $this->streetNumber = $streetNumber; + + return $this; + } + + public function getStreetName(): string + { + return $this->streetName; + } + + public function setStreetName(string $streetName): self + { + $this->streetName = $streetName; + + return $this; + } + + public function getLocality(): string + { + return $this->locality; + } + + public function hasLocality(): bool + { + return $this->locality !== ''; + } + + public function setLocality(string $locality): self + { + $this->locality = $locality; + + return $this; + } + + public function getSubLocality(): ?string + { + return $this->subLocality; + } + + public function setSubLocality(?string $subLocality): self + { + $this->subLocality = $subLocality; + + return $this; + } + + public function getPostalCode(): string + { + return $this->postalCode; + } + + public function setPostalCode(string $postalCode): self + { + $this->postalCode = $postalCode; + + return $this; + } + + public function getCountry(): string + { + return $this->country; + } + + public function setCountry(string $country): self + { + $this->country = $country; + + return $this; + } + + public function getLatitude(): string + { + return $this->latitude; + } + + public function setLatitude(string $latitude): self + { + $this->latitude = $latitude; + + return $this; + } + + public function getLongitude(): string + { + return $this->longitude; + } + + public function setLongitude(string $longitude): self + { + $this->longitude = $longitude; + + return $this; + } + + public function getProvidedBy(): string + { + return $this->providedBy; + } + + public function setProvidedBy(string $providedBy): Address + { + $this->providedBy = $providedBy; + + return $this; + } + + public function getAttribution(): string + { + return $this->attribution; + } + + public function setAttribution(string $attribution): Address + { + $this->attribution = $attribution; + + return $this; + } + + public function getOsmType(): ?string + { + return $this->osmType; + } + + public function setOsmType(?string $osmType): Address + { + $this->osmType = $osmType; + + return $this; + } + + public function getOsmId(): int + { + return $this->osmId; + } + + public function setOsmId(int $osmId): Address + { + $this->osmId = $osmId; + + return $this; + } + + // End of basic setters/getters ———————————————————————————————————————————— + + /** + * Format a full address with the user input (for address update step2). + */ + public function getFullAddress(): string + { + $addressSupplement = u($this->addressSupplement)->isEmpty() ? '' : ', '.$this->addressSupplement; + + return $this->address.$addressSupplement.', '.$this->postalCode.', '.$this->locality.', '.$this->country; + } + + /** + * Override the properties of the old address. + */ + public function setFromAddressUpdateStep1(Address $newAddress): self + { + $this->setAddress($newAddress->getAddress()); + $this->setAddressSupplement($newAddress->getAddressSupplement()); + $this->setPostalCode($newAddress->getPostalCode()); + $this->setLocality($newAddress->getLocality()); + $this->setCountry($newAddress->getCountry()); + + return $this; + } + + public function getSubAndLocality(): string + { + if (u($this->subLocality)->isEmpty()) { + return $this->locality; + } + + return $this->locality.' ('.$this->subLocality.')'; + } +} diff --git a/src/Entity/Category.php b/src/Entity/Category.php new file mode 100644 index 0000000..b44f33d --- /dev/null +++ b/src/Entity/Category.php @@ -0,0 +1,252 @@ + $children + */ + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] + private Collection $children; + + #[Gedmo\TreeLeft] + #[ORM\Column(name: 'lft', type: Types::INTEGER)] + private int $lft; + + #[Gedmo\TreeLevel] + #[ORM\Column(name: 'lvl', type: Types::INTEGER)] + private int $lvl; + + #[Gedmo\TreeRight] + #[ORM\Column(name: 'rgt', type: Types::INTEGER)] + private int $rgt; + + /** + * Associated product type. Object or service. + */ + #[ORM\Column(name: 'type', type: 'string', nullable: false, enumType: ProductType::class)] + #[Assert\NotBlank] + #[Gedmo\TreeRoot(identifierMethod: 'getType')] + protected ProductType $type; + + /** + * Short and main name of the category. + */ + #[ORM\Column(type: Types::STRING, length: 255, nullable: false)] + #[Assert\NotBlank] + #[Assert\Length(max: 255)] + private string $name; + + /** + * SEO friendly name for URLs. + */ + #[ORM\Column(length: 255, unique: false)] + #[Gedmo\Slug(fields: ['name'])] + private string $slug; + + /** + * Tells if the category is visible on the search form. + */ + #[ORM\Column(type: 'boolean', nullable: false)] + protected bool $enabled = true; + + /** + * Default image for the objects associated to the category when not having + * specific images. + */ + #[ORM\Column(nullable: true)] + private ?string $image = null; + + public function __construct() + { + $this->children = new ArrayCollection(); + } + + public function __toString(): string + { + return $this->name; + } + + public function getId(): Uuid + { + return $this->id; + } + + public function setId(Uuid $uuid): self + { + $this->id = $uuid; + + return $this; + } + + public function getParent(): ?self + { + return $this->parent; + } + + public function setParent(?self $parent): self + { + $this->parent = $parent; + + return $this; + } + + /** + * @return Collection + */ + public function getChildren(): Collection + { + return $this->children; + } + + public function hasChildren(): bool + { + return !$this->children->isEmpty(); + } + + public function addChild(self $category): self + { + if (!$this->children->contains($category)) { + $this->children->add($category); + $category->setParent($this); + } + + return $this; + } + + public function removeChild(self $category): self + { + if ($this->children->removeElement($category)) { + // set the owning side to null (unless already changed) + if ($category->getParent() === $this) { + $category->setParent(null); + } + } + + return $this; + } + + public function getType(): ProductType + { + return $this->type; + } + + public function setType(ProductType $type): self + { + $this->type = $type; + + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function setSlug(string $slug): self + { + $this->slug = $slug; + + return $this; + } + + public function getSlug(): string + { + return $this->slug; + } + + public function isEnabled(): bool + { + return $this->enabled; + } + + public function setEnabled(bool $enabled): self + { + $this->enabled = $enabled; + + return $this; + } + + public function getImage(): ?string + { + return $this->image; + } + + public function setImage(?string $image): self + { + $this->image = $image; + + return $this; + } + + /** + * Create a dummy empty object with non nullable fields initialized. + */ + public static function getForEmptyData(): Category + { + return (new self()) + ->setId(Uuid::v6()) + ->setType(ProductType::OBJECT) + ->setName('') + ->setSlug(''); + } + + /** End of basic 'etters ———————————————————————————————————————————————— */ + public function hasParent(): bool + { + return $this->parent !== null; + } + + public function getNameWithIndent(): string + { + return $this->hasParent() ? str_repeat('—', $this->lvl).'> '.$this->getName() : $this->getName(); + } +} diff --git a/src/Entity/Configuration.php b/src/Entity/Configuration.php new file mode 100644 index 0000000..89415b4 --- /dev/null +++ b/src/Entity/Configuration.php @@ -0,0 +1,151 @@ +> + */ + #[ORM\Column(type: 'json')] + private array $configuration = []; + + public function getId(): ?int + { + return $this->id; + } + + public function getType(): ConfigurationType + { + return $this->type; + } + + public function setType(ConfigurationType $type): self + { + $this->type = $type; + + return $this; + } + + /** + * @return array> + */ + public function getConfiguration(): array + { + return $this->configuration; + } + + /** + * @param array> $configuration + */ + public function setConfiguration(array $configuration): self + { + $this->configuration = $configuration; + + return $this; + } + + public static function getInstanceConfiguration(): Configuration + { + return (new self())->setType(ConfigurationType::INSTANCE); + } + + /** end of basic getters and setters ------------------------------------------------ */ + + /** + * @return array + */ + public function getNotificationsSender(): array + { + /** @var array $notificationsSender */ + $notificationsSender = $this->configuration['notificationsSender'] ?? []; + + return $notificationsSender; + } + + public function getNotificationsSenderEmail(): string + { + $notificationsSender = $this->getNotificationsSender(); + + return $notificationsSender['notificationsSenderEmail'] ?? ''; + } + + public function getNotificationsSenderName(): string + { + $notificationsSender = $this->getNotificationsSender(); + + return $notificationsSender['notificationsSenderName'] ?? ''; + } + + /** + * @return array + */ + public function getContactInformations(): array + { + /** @var array $contactInfo */ + $contactInfo = $this->configuration['contact'] ?? []; + + return $contactInfo; + } + + public function getContactEnabled(): bool + { + /** @var array $contactEnabled */ + $contactEnabled = $this->getContactInformations(); + + return $contactEnabled['contactFormEnabled']; + } + + public function getContactEmail(): string + { + /** @var array $contactEnabled + */ + $contactEnabled = $this->getContactInformations(); + + return $contactEnabled['contactFormEmail']; + } + + public function isConversationAdminAccessible(): bool + { + /** @var array $config */ + $config = $this->configuration['confidentiality'] ?? []; + + return $config['confidentialityConversationAdminAccess']; + } + + public function isGroupsCreationForAll(): bool + { + return $this->configuration['groups']['groupsCreationMode'] === ParametersFormCommand::ALL; + } + + // for test only + public function setGroupsCreationModeToAdminOnly(): self + { + $this->configuration['groups']['groupsCreationMode'] = ParametersFormCommand::ONLY_ADMIN; + + return $this; + } +} diff --git a/src/Entity/Group.php b/src/Entity/Group.php new file mode 100755 index 0000000..fa86764 --- /dev/null +++ b/src/Entity/Group.php @@ -0,0 +1,387 @@ + $children + */ + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] + private Collection $children; + + /** + * membership of the group. It can be free or paying. + */ + #[ORM\Column(name: 'membership', type: 'string', nullable: false, enumType: GroupMembership::class)] + protected GroupMembership $membership = GroupMembership::FREE; + + /** + * If true, administrators can send invitations. + */ + #[ORM\Column(type: 'boolean', nullable: false)] + private bool $invitationByAdmin = false; + + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'group', targetEntity: UserGroup::class, orphanRemoval: true)] + private Collection $userGroups; + + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'group', targetEntity: GroupOffer::class, orphanRemoval: true)] + #[ORM\OrderBy(['price' => 'ASC'])] + private Collection $offers; + + /** + * List of visible product in the group. + * + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: Product::class, mappedBy: 'groups')] + private Collection $products; + + public function __construct() + { + $this->children = new ArrayCollection(); + $this->userGroups = new ArrayCollection(); + $this->offers = new ArrayCollection(); + $this->products = new ArrayCollection(); + } + + public function __toString(): string + { + return $this->name; + } + + public function getId(): Uuid + { + return $this->id; + } + + public function setId(Uuid $uuid): self + { + $this->id = $uuid; + + return $this; + } + + public function getParent(): ?self + { + return $this->parent; + } + + public function setParent(?self $parent): self + { + $this->parent = $parent; + + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function setSlug(string $slug): self + { + $this->slug = $slug; + + return $this; + } + + public function getSlug(): string + { + return $this->slug; + } + + public function getType(): GroupType + { + return $this->type; + } + + public function setType(GroupType $type): Group + { + $this->type = $type; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): self + { + $this->description = $description; + + return $this; + } + + public function getUrl(): ?string + { + return $this->url; + } + + public function setUrl(?string $url): self + { + $this->url = $url; + + return $this; + } + + /** + * @return Collection + */ + public function getChildren(): Collection + { + return $this->children; + } + + public function addChild(self $group): self + { + if (!$this->children->contains($group)) { + $this->children->add($group); + $group->setParent($this); + } + + return $this; + } + + public function removeChild(self $group): self + { + // set the owning side to null (unless already changed) + if ($this->children->removeElement($group) && $group->getParent() === $this) { + $group->setParent(null); + } + + return $this; + } + + public function getMembership(): GroupMembership + { + return $this->membership; + } + + public function setMembership(GroupMembership $membership): void + { + $this->membership = $membership; + } + + public function isInvitationByAdmin(): bool + { + return $this->invitationByAdmin; + } + + public function setInvitationByAdmin(bool $invitationByAdmin): self + { + $this->invitationByAdmin = $invitationByAdmin; + + return $this; + } + + /** + * @return Collection + */ + public function getUserGroups(): Collection + { + return $this->userGroups; + } + + public function addUserGroup(UserGroup $userGroup): self + { + if (!$this->userGroups->contains($userGroup)) { + $this->userGroups->add($userGroup); + $userGroup->setGroup($this); + } + + return $this; + } + + public function removeUserGroup(UserGroup $userGroup): self + { + $this->userGroups->removeElement($userGroup); + + return $this; + } + + /** + * @return Collection + */ + public function getOffers(): Collection + { + return $this->offers; + } + + /** + * @param Collection $offers + */ + public function setOffers(Collection $offers): Group + { + $this->offers = $offers; + + return $this; + } + + /** + * @return Collection + */ + public function getProducts(): Collection + { + return $this->products; + } + + public function addProduct(Product $product): self + { + if (!$this->products->contains($product)) { + $this->products->add($product); + $product->addGroup($this); + } + + return $this; + } + + public function removeProduct(Product $product): self + { + if ($this->products->removeElement($product)) { + $product->removeGroup($this); + } + + return $this; + } + + // End of basic 'etters ---------------------------------------------------- + + /** + * @return array + */ + public function getRoutingParameters(): array + { + return [ + 'id' => (string) $this->getId(), + 'slug' => $this->getSlug(), + ]; + } + + /** + * Test if a given user is main admin of the group. + */ + public function isMainAdmin(User $user): bool + { + $mainAdminUserGroups = $this->userGroups->filter( + static fn (UserGroup $userGroup) => $userGroup->getUser() === $user && $userGroup->isMainAdminAccount() + ); + + return !$mainAdminUserGroups->isEmpty(); + } + + /** + * Get active offers only. + * + * @return Collection + */ + public function getActiveOffers(): Collection + { + /** @var Collection $collection */ + $collection = $this->offers->filter( + static fn (GroupOffer $groupOffer) => $groupOffer->isActive() + ); + + return $collection; + } + + public function hasActiveOffers(): bool + { + return !$this->getActiveOffers()->isEmpty(); + } +} diff --git a/src/Entity/GroupOffer.php b/src/Entity/GroupOffer.php new file mode 100644 index 0000000..87263c1 --- /dev/null +++ b/src/Entity/GroupOffer.php @@ -0,0 +1,172 @@ + 'ASC'])] + private Group $group; + + /** + * Short name of the offer. + */ + #[ORM\Column(type: Types::STRING, length: 255, nullable: false)] + #[Assert\NotBlank] + #[Assert\Length(max: 255)] + private string $name; + + /** + * Type of offer. + */ + #[ORM\Column(name: 'type', type: 'string', nullable: false, enumType: GroupOfferType::class)] + #[Assert\NotBlank] + protected GroupOfferType $type; + + /** + * Price, we stored the amount multiplied by 100 so we can use an integer for + * this property. + */ + #[ORM\Column(type: Types::INTEGER, nullable: false)] + protected int $price; + + /** + * Associated currency for the price property. + * + * @see https://en.wikipedia.org/wiki/ISO_4217 + */ + #[ORM\Column(type: Types::STRING, nullable: false)] + protected string $currency = self::DEFAULT_CURRENCY; + + /** + * If the offer is visible on the front site. Can be use to deactivate offers + * for some time. + */ + #[ORM\Column(type: 'boolean', nullable: false)] + protected bool $active = true; + + public function __toString() + { + return $this->name.' ('.$this->type->value.')'; + } + + public function getId(): Uuid + { + return $this->id; + } + + public function setId(Uuid $uuid): self + { + $this->id = $uuid; + + return $this; + } + + public function getGroup(): Group + { + return $this->group; + } + + public function setGroup(Group $group): GroupOffer + { + $this->group = $group; + + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): GroupOffer + { + $this->name = $name; + + return $this; + } + + public function getType(): GroupOfferType + { + return $this->type; + } + + public function setType(GroupOfferType $type): GroupOffer + { + $this->type = $type; + + return $this; + } + + public function setPrice(int $price): self + { + $this->price = $price; + + return $this; + } + + public function getPrice(): int + { + return $this->price; + } + + public function getActualPrice(): int + { + return $this->price / 100; + } + + public function getCurrency(): string + { + return $this->currency; + } + + public function setCurrency(string $currency): self + { + $this->currency = $currency; + + return $this; + } + + public function isActive(): bool + { + return $this->active; + } + + public function setActive(bool $active): GroupOffer + { + $this->active = $active; + + return $this; + } +} diff --git a/src/Entity/ImageInterface.php b/src/Entity/ImageInterface.php new file mode 100644 index 0000000..7587843 --- /dev/null +++ b/src/Entity/ImageInterface.php @@ -0,0 +1,10 @@ +|null + */ + public function getImages(): ?array; +} diff --git a/src/Entity/Menu.php b/src/Entity/Menu.php new file mode 100644 index 0000000..ac66fee --- /dev/null +++ b/src/Entity/Menu.php @@ -0,0 +1,144 @@ + $items + */ + #[ORM\OneToMany(mappedBy: 'menu', targetEntity: MenuItem::class, cascade: ['persist', 'remove', 'detach'])] + private Collection $items; + + public function __construct() + { + $this->items = new ArrayCollection(); + } + + public function __toString(): string + { + return $this->code; + } + + public function getId(): int + { + return $this->id; + } + + public function setId(int $id): self + { + $this->id = $id; + + return $this; + } + + public function getLogo(): ?string + { + return $this->logo; + } + + public function setLogo(?string $logo): self + { + $this->logo = $logo; + + return $this; + } + + public function getCode(): string + { + return $this->code; + } + + public function setCode(string $code): self + { + $this->code = $code; + + return $this; + } + + public function getImage(): ?string + { + return $this->getLogo(); + } + + /** + * @return Collection + */ + public function getItems(): Collection + { + return $this->items; + } + + /** + * @param Collection $items + */ + public function setItems(Collection $items): self + { + $this->items = $items; + + return $this; + } + + public function addItem(MenuItem $item): self + { + if (!$this->items->contains($item)) { + $this->items->add($item); + $item->setMenu($this); + } + + return $this; + } + + public function removeItem(MenuItem $item): self + { + $this->items->removeElement($item); +// if ($this->items->removeElement($item)) { + // set the owning side to null (unless already changed) +// if ($item->getMenu() === $this) { +// $item->setMenu(null); +// } +// } + + return $this; + } + + public function itemsCount(): int + { + return $this->items->count(); + } +} diff --git a/src/Entity/MenuItem.php b/src/Entity/MenuItem.php new file mode 100644 index 0000000..e92983a --- /dev/null +++ b/src/Entity/MenuItem.php @@ -0,0 +1,255 @@ + $children + */ + #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] + #[ORM\OrderBy(['position' => 'ASC'])] + private Collection $children; + + /** + * Menu related to items. + */ + #[Gedmo\SortableGroup] + #[ORM\ManyToOne(targetEntity: Menu::class, inversedBy: 'items')] + private Menu $menu; + + /** + * Position of the item in the front menu. + */ + #[Gedmo\SortablePosition] + #[ORM\Column(name: 'position', type: 'integer')] + private int $position; + + public function __construct() + { + $this->children = new ArrayCollection(); + } + + public function __toString(): string + { + return $this->name ?? $this->link; + } + + public function getId(): Uuid + { + return $this->id; + } + + public function setId(Uuid $uuid): self + { + $this->id = $uuid; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getLinkType(): LinkType + { + return $this->linkType; + } + + public function setLinkType(LinkType $linkType): void + { + $this->linkType = $linkType; + } + + public function getMediaType(): ?SocialMediaType + { + return $this->mediaType; + } + + public function setMediaType(?SocialMediaType $mediaType): void + { + $this->mediaType = $mediaType; + } + + public function getLink(): string + { + return $this->link; + } + + public function setLink(string $link): self + { + $this->link = $link; + + return $this; + } + + public function getParent(): ?self + { + return $this->parent; + } + + public function setParent(?self $parent): self + { + $this->parent = $parent; + + return $this; + } + + /** + * @return Collection + */ + public function getChildren(): Collection + { + return $this->children; + } + + public function hasChildren(): bool + { + return !$this->children->isEmpty(); + } + + /** + * @param Collection $children + */ + public function setChildren(Collection $children): self + { + $this->children = $children; + + return $this; + } + + public function getMenu(): Menu + { + return $this->menu; + } + + public function setMenu(Menu $menu): MenuItem + { + $this->menu = $menu; + + return $this; + } + + public function getPosition(): int + { + return $this->position; + } + + public function setPosition(int $position): void + { + $this->position = $position; + } + + public function getPositionHuman(): int + { + return $this->position + 1; + } + + public function isFirst(): bool + { + return $this->position === self::POSITION_FIRST; + } + + public function up(): int + { + return $this->position - 1; + } + + public function down(): int + { + return $this->position + 1; + } + + public function isLink(): bool + { + return $this->linkType === LinkType::LINK; + } + + public function isSocialNetwork(): bool + { + return $this->linkType === LinkType::SOCIAL_NETWORK; + } +} diff --git a/src/Entity/Message.php b/src/Entity/Message.php new file mode 100644 index 0000000..eb12afe --- /dev/null +++ b/src/Entity/Message.php @@ -0,0 +1,267 @@ + + */ + #[ORM\Column(type: 'json')] + protected array $messageParameters = []; + + /** + * Final message content. + */ + #[ORM\Column(type: 'text', nullable: false)] + #[Assert\NotBlank(groups: [NewMessageType::class])] + #[Assert\Length(max: 1000, groups: [NewMessageType::class])] + protected string $message; + + /** + * If the message was read by the owner. + */ + #[ORM\Column] + protected bool $ownerRead = false; + + /** + * Date the message was read by the owner. + */ + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + protected ?\DateTimeImmutable $ownerReadAt = null; + + /** + * If the message was read by the recipient. + */ + #[ORM\Column] + protected bool $recipientRead = false; + + /** + * Date the message was read by the owner. + */ + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + protected ?\DateTimeImmutable $recipientReadAt = null; + + public function __toString(): string + { + return $this->message; + } + + public function getId(): Uuid + { + return $this->id; + } + + public function setId(Uuid $id): Message + { + $this->id = $id; + + return $this; + } + + public function getServiceRequest(): ServiceRequest + { + return $this->serviceRequest; + } + + public function setServiceRequest(ServiceRequest $serviceRequest): Message + { + $this->serviceRequest = $serviceRequest; + + return $this; + } + + public function getType(): MessageType + { + return $this->type; + } + + public function setType(MessageType $type): Message + { + $this->type = $type; + + return $this; + } + + public function getMessage(): string + { + return $this->message; + } + + public function setMessage(string $message): Message + { + $this->message = $message; + + return $this; + } + + public function getMessageTemplate(): ?string + { + return $this->messageTemplate; + } + + public function setMessageTemplate(?string $messageTemplate): Message + { + $this->messageTemplate = $messageTemplate; + + return $this; + } + + /** + * @return array + */ + public function getMessageParameters(): array + { + return $this->messageParameters; + } + + /** + * @param array $messageParameters + */ + public function setMessageParameters(array $messageParameters): Message + { + $this->messageParameters = $messageParameters; + + return $this; + } + + public function isOwnerRead(): bool + { + return $this->ownerRead; + } + + public function setOwnerRead(bool $ownerRead): Message + { + $this->ownerRead = $ownerRead; + + return $this; + } + + public function getOwnerReadAt(): ?\DateTimeImmutable + { + return $this->ownerReadAt; + } + + public function setOwnerReadAt(?\DateTimeImmutable $ownerReadAt): Message + { + $this->ownerReadAt = $ownerReadAt; + + return $this; + } + + public function isRecipientRead(): bool + { + return $this->recipientRead; + } + + public function setRecipientRead(bool $recipientRead): Message + { + $this->recipientRead = $recipientRead; + + return $this; + } + + public function getRecipientReadAt(): ?\DateTimeImmutable + { + return $this->recipientReadAt; + } + + public function setRecipientReadAt(?\DateTimeImmutable $recipientReadAt): Message + { + $this->recipientReadAt = $recipientReadAt; + + return $this; + } + + // end of basic 'etters ———————————————————————————————————————————————————— + + /** + * Get the recipient of the message dependning on this type. + */ + public function getSender(): User + { + if ($this->type->isFromOwner()) { + return $this->serviceRequest->getOwner(); + } + + if ($this->type->isFromRecipient()) { + return $this->serviceRequest->getRecipient(); + } + + throw new \LogicException('Cannot get recipient for a system message'); + } + + /** + * Get the sender of the message depending on this type. + */ + public function getRecipient(): User + { + if ($this->type->isFromOwner()) { + return $this->serviceRequest->getRecipient(); + } + + if ($this->type->isFromRecipient()) { + return $this->serviceRequest->getOwner(); + } + + throw new \LogicException('Cannot get recipient for a system message'); + } +} diff --git a/src/Entity/Page.php b/src/Entity/Page.php new file mode 100644 index 0000000..c46d330 --- /dev/null +++ b/src/Entity/Page.php @@ -0,0 +1,144 @@ +name; + } + + public function getId(): Uuid + { + return $this->id; + } + + public function setId(Uuid $uuid): self + { + $this->id = $uuid; + + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function setSlug(string $slug): self + { + $this->slug = $slug; + + return $this; + } + + public function getSlug(): string + { + return $this->slug; + } + + public function getContent(): string + { + return $this->content; + } + + public function setContent(string $content): Page + { + $this->content = $content; + + return $this; + } + + public function isEnabled(): bool + { + return $this->enabled; + } + + public function setEnabled(bool $enabled): self + { + $this->enabled = $enabled; + + return $this; + } + + public function isHome(): bool + { + return $this->home; + } + + public function setHome(bool $home): self + { + $this->home = $home; + + return $this; + } +} diff --git a/src/Entity/Payment.php b/src/Entity/Payment.php new file mode 100644 index 0000000..5effc21 --- /dev/null +++ b/src/Entity/Payment.php @@ -0,0 +1,91 @@ +id; + } + + public function setId(string $id): Payment + { + $this->id = $id; + + return $this; + } + + public function getUser(): User + { + return $this->user; + } + + public function setUser(User $user): Payment + { + $this->user = $user; + + return $this; + } + + public function getMethod(): string + { + return $this->details['method'] ?? 'NA'; + } + + public function isPaid(): bool + { + // offline payment + if (\array_key_exists(Constants::FIELD_PAID, $this->details)) { + return $this->details[Constants::FIELD_PAID] ?? false; + } + + // test and prod mode + + return $this->getStatus() === Constants::FIELD_PAID; + } + + public function getStatus(): ?string + { + // offline + if (\array_key_exists(Constants::FIELD_STATUS, $this->details)) { + return $this->details[Constants::FIELD_STATUS] ?? null; + } + + // test and prod mode + + return $this->details['payment'][Constants::FIELD_STATUS] ?? null; + } +} diff --git a/src/Entity/PaymentToken.php b/src/Entity/PaymentToken.php new file mode 100644 index 0000000..1d3896d --- /dev/null +++ b/src/Entity/PaymentToken.php @@ -0,0 +1,21 @@ + 'Swicth the status of the product'], + normalizationContext: ['groups' => [ProductSwitchProcessor::class]], + security: "is_granted('".ProductVoter::EDIT."', object)", + input: false, + processor: ProductSwitchProcessor::class, + ), + ] +)] +class Product implements \Stringable, ImagesInterface +{ + use TimestampableEntity; + use ProductObjectTrait; + use ProductServiceTrait; + use i18nTrait; + + final public const DEFAULT_CURRENCY = 'EUR'; + + /** + * Generates a V6 uuid. + */ + #[ORM\Id] + #[ORM\Column(type: 'uuid', unique: true)] + #[ORM\GeneratedValue(strategy: 'CUSTOM')] + #[ORM\CustomIdGenerator(class: UuidGenerator::class)] + #[ApiProperty(identifier: true)] + #[Groups([ProductSwitchProcessor::class])] + private Uuid $id; + + /** + * Type of the product. It can be an object to lend or a service. + */ + #[ORM\Column(name: 'type', type: 'string', nullable: false, enumType: ProductType::class)] + #[Assert\NotBlank] + protected ProductType $type; + + /** + * Main category of the product (1st or second level). + */ + #[ORM\ManyToOne(targetEntity: Category::class)] + #[ORM\JoinColumn(referencedColumnName: 'id', nullable: false)] + #[Assert\NotBlank(groups: [AbstractProductFormType::class])] + private Category $category; + + /** + * Status of the product. + */ + #[ORM\Column(name: 'status', type: 'string', nullable: false, enumType: ProductStatus::class)] + #[Assert\NotBlank] + #[Groups([ProductSwitchProcessor::class])] + protected ProductStatus $status; + + /** + * Visibility of the product. + */ + #[ORM\Column(name: 'visibility', type: 'string', nullable: false, enumType: ProductVisibility::class)] + #[Assert\NotBlank] + protected ProductVisibility $visibility = ProductVisibility::PUBLIC; + + /** + * User that owns the product or propose the service. + */ + #[ORM\ManyToOne(targetEntity: User::class)] + #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] // if the owner is deleted then the product will also be deleted without constraint error + #[Assert\NotBlank] + protected User $owner; + + /** + * Short and main name of the product. + */ + #[ORM\Column(type: Types::STRING, length: 255, nullable: false)] + #[Assert\NotBlank(groups: [AbstractProductFormType::class])] + #[Assert\Length(max: 255, groups: [AbstractProductFormType::class, 'default'])] + private string $name; + + /** + * SEO friendly name for URLs. + */ + #[ORM\Column(length: 255, unique: false)] + #[Gedmo\Slug(fields: ['name'])] + private string $slug; + + /** + * Longer description of the product. + */ + #[ORM\Column(type: Types::TEXT, nullable: true)] + #[Assert\NotBlank(groups: [AbstractProductFormType::class])] + #[Assert\Length(max: 2000, groups: [AbstractProductFormType::class])] + private ?string $description = null; + + /** + * User images for the product. + * + * @var array + */ + #[ORM\Column(type: 'json', nullable: true)] + private ?array $images = null; + + /** + * @var Collection $availabilities + */ + #[ORM\OneToMany(mappedBy: 'product', targetEntity: ProductAvailability::class, cascade: ['persist', 'remove', 'detach'])] + #[ORM\OrderBy(['startAt' => 'ASC'])] + private Collection $availabilities; + + /** + * @var Collection $serviceRequests + */ + #[ORM\OneToMany(mappedBy: 'product', targetEntity: ServiceRequest::class)] + private Collection $serviceRequests; + + /** + * If the product in not public then the list of group the product is visible. + * + * @var Collection $groups + */ + #[ORM\ManyToMany(targetEntity: Group::class, inversedBy: 'products')] + #[Assert\When( + expression: '!this.getVisibility().isPublic()', + constraints: [ + new Assert\Count(min: 1, minMessage: 'app.entity.product.groups.constraints.count.min_message'), + ], + groups: [AbstractProductFormType::class], + )] + private Collection $groups; + + /** + * This is a virtual field to store the distance with a given location when + * using a proximity filter. + */ + private ?int $geoDistance = null; + + /** + * Get distance in meters. + */ + public function setGeoDistance(?int $geoDistance): self + { + $this->geoDistance = $geoDistance; + + return $this; + } + + /** + * Return kilometers. + */ + public function getGeoDistance(): ?float + { + return $this->geoDistance !== null ? $this->geoDistance / 1000 : null; + } + + public function __construct() + { + $this->availabilities = new ArrayCollection(); + $this->serviceRequests = new ArrayCollection(); + $this->groups = new ArrayCollection(); + } + + public function __toString(): string + { + return $this->name; + } + + public function getId(): Uuid + { + return $this->id; + } + + public function setId(Uuid $uuid): self + { + $this->id = $uuid; + + return $this; + } + + public function getType(): ProductType + { + return $this->type; + } + + public function setType(ProductType $type): self + { + $this->type = $type; + + return $this; + } + + public function getCategory(): Category + { + return $this->category; + } + + public function setCategory(Category $category): self + { + $this->category = $category; + + return $this; + } + + public function getStatus(): ProductStatus + { + return $this->status; + } + + public function setStatus(ProductStatus $status): Product + { + $this->status = $status; + + return $this; + } + + public function getVisibility(): ProductVisibility + { + return $this->visibility; + } + + public function setVisibility(ProductVisibility $visibility): self + { + $this->visibility = $visibility; + + return $this; + } + + public function getOwner(): User + { + return $this->owner; + } + + public function isOwner(User $user): bool + { + return $this->owner === $user; + } + + public function setOwner(User $owner): self + { + $this->owner = $owner; + + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function setSlug(string $slug): self + { + $this->slug = $slug; + + return $this; + } + + public function getSlug(): string + { + return $this->slug; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): self + { + $this->description = $description; + + return $this; + } + + public function getImages(): ?array + { + return $this->images; + } + + public function getFirstImage(): ?string + { + return $this->images[0] ?? null; + } + + /** + * @param array|null $images + */ + public function setImages(?array $images): self + { + $this->images = array_values(array_filter($images ?? [])); // make sure we don't save null or empty values + + return $this; + } + + /** + * @param array $images + */ + public function addImages(array $images): self + { + $this->images = array_merge($this->images ?? [], $images); + + return $this; + } + + /** + * @return Collection + */ + public function getAvailabilities(): Collection + { + return $this->availabilities; + } + + /** + * @param Collection $availabilities + */ + public function setAvailabilities(Collection $availabilities): self + { + $this->availabilities = $availabilities; + + return $this; + } + + public function addAvailability(ProductAvailability $productAvailability): self + { + if (!$this->availabilities->contains($productAvailability)) { + $this->availabilities->add($productAvailability); + $productAvailability->setProduct($this); + } + + return $this; + } + + public function removeAvailability(ProductAvailability $productAvailability): self + { + $this->availabilities->removeElement($productAvailability); + + return $this; + } + + /** + * @return Collection + */ + public function getServiceRequests(): Collection + { + return $this->serviceRequests; + } + + /** + * @param Collection $serviceRequests + */ + public function setServiceRequests(Collection $serviceRequests): void + { + $this->serviceRequests = $serviceRequests; + } + + /** + * @return Collection + */ + public function getGroups(): Collection + { + return $this->groups; + } + + /** + * @return array + */ + public function getGroupsIds(): array + { + return $this->getGroups()->map(fn (Group $group) => (string) $group->getId())->toArray(); + } + + public function addGroup(Group $group): self + { + if (!$this->groups->contains($group)) { + $this->groups->add($group); + } + + return $this; + } + + public function removeGroup(Group $group): self + { + $this->groups->removeElement($group); + + return $this; + } + + /** + * Remove all associated groups. + */ + public function removeGroups(): self + { + $this->groups->clear(); + + return $this; + } + + public function setPublic(): self + { + $this->visibility = ProductVisibility::PUBLIC; + + return $this; + } + + public function setPaused(): self + { + $this->status = ProductStatus::PAUSED; + + return $this; + } + + public function setActive(): self + { + $this->status = ProductStatus::ACTIVE; + + return $this; + } + + /* End of basic getters/setters ========================================= */ + + public function isActive(): bool + { + return $this->getStatus()->isActive(); + } + + public function isPaused(): bool + { + return $this->getStatus()->isPaused(); + } + + public function switchStatus(): self + { + $this->setStatus($this->isActive() ? ProductStatus::PAUSED : ProductStatus::ACTIVE); + + return $this; + } + + /** + * Return the unavailabilities of the product as a simple array of dates "2023-02-09". + * + * @return array + */ + public function getUnavailabilities(?ServiceRequest $serviceRequest = null): array + { + /** @var array $resultArray */ + $resultArray = []; + $today = CarbonImmutable::today(); // start of day 00:00:00 + $unavailabilities = $this->getAvailabilities()->filter( + fn (ProductAvailability $pa) => $pa->getMode()->isUnavailable() && // of the good type + ($serviceRequest === null || $pa->getServiceRequest() !== $serviceRequest) && // exclude the dates of the current service request (modify dates) + $pa->getEndAt() >= $today // passed dates are useless but the start date can be in the past + ); + + foreach ($unavailabilities as $unavailability) { + /** @var ProductAvailability $unavailability */ + $period = CarbonInterval::days()->toPeriod($unavailability->getStartAt(), $unavailability->getEndAt()); + $resultArray = array_merge($resultArray, $period->toArray()); + } + $resultArray = array_map(static fn (CarbonInterface $date) => $date->format('Y-m-d'), $resultArray); + sort($resultArray); + + return array_unique($resultArray); + } + + /** + * Get 1st level category. + */ + public function getMainCategory(): Category + { + return $this->category->getParent() ?? $this->category; + } + + /** + * Get subcategory, it is the current category if it is a child. + */ + public function getSubCategory(): ?Category + { + return $this->category->hasParent() ? $this->category : null; + } + + public function createServiceRequest(User $recipient, \DateTimeImmutable $startAt, \DateTimeImmutable $endAt): ServiceRequest + { + return (new ServiceRequest()) + ->setOwner($this->getOwner()) + ->setProduct($this) + ->setRecipient($recipient) + ->setStartAt($startAt) + ->setEndAt($endAt); + } + + public function duplicate(): self + { + return (new Product()) + ->setType($this->getType()) + ->setCategory($this->getCategory()) + ->setOwner($this->getOwner()) + ->setType($this->getType()) + ->setStatus($this->getStatus()) + ->setVisibility($this->getVisibility()) + ->setDescription($this->getDescription()) + ->setAge($this->getAge()) + ->setDeposit($this->getDeposit()) + ->setCurrency($this->getCurrency()) + ->setPreferredLoanDuration($this->getPreferredLoanDuration()) + ->setDuration($this->getDuration()); + } + + public function deleteImage(string $image): self + { + $images = array_flip($this->images ?? []); + unset($images[$image]); + $this->images = array_values(array_flip($images)); + + return $this; + } + + public function delete(): self + { + $this->status = ProductStatus::DELETED; + + return $this; + } + + public function hasServiceRequests(): bool + { + return !$this->serviceRequests->isEmpty(); + } + + public function hasOngoingServiceRequests(): bool + { + $ongoing = $this->serviceRequests->filter( + fn (ServiceRequest $serviceRequest) => $serviceRequest->getStatus()->isOngoing() + ); + + return !$ongoing->isEmpty(); + } + + /** + * A product is indexable if it is active and the owner has not activated the + * vacation mode. + */ + public function isIndexable(): bool + { + return $this->status->isIndexable() + && $this->owner->isIndexable() + ; + } + + /** + * @return array + */ + public function getRoutingParameters(): array + { + return [ + 'id' => (string) $this->getId(), + 'slug' => $this->getSlug(), + ]; + } +} diff --git a/src/Entity/ProductAvailability.php b/src/Entity/ProductAvailability.php new file mode 100644 index 0000000..62a9dee --- /dev/null +++ b/src/Entity/ProductAvailability.php @@ -0,0 +1,173 @@ += startDate + */ + #[ORM\Column(type: Types::DATETIME_IMMUTABLE)] + protected \DateTimeImmutable $endAt; + + public function __toString(): string + { + return $this->product->getName().' / '.$this->startAt->format('Y-m-d').' / '.$this->endAt->format('Y-m-d'); + } + + public function getId(): Uuid + { + return $this->id; + } + + public function setId(Uuid $id): ProductAvailability + { + $this->id = $id; + + return $this; + } + + public function getProduct(): Product + { + return $this->product; + } + + public function setProduct(Product $product): ProductAvailability + { + $this->product = $product; + + return $this; + } + + public function getServiceRequest(): ?ServiceRequest + { + return $this->serviceRequest; + } + + public function setServiceRequest(?ServiceRequest $serviceRequest): ProductAvailability + { + $this->serviceRequest = $serviceRequest; + + return $this; + } + + public function getMode(): ProductAvailabilityMode + { + return $this->mode; + } + + public function setMode(ProductAvailabilityMode $mode): ProductAvailability + { + $this->mode = $mode; + + return $this; + } + + public function getType(): ProductAvailabilityType + { + return $this->type; + } + + public function setType(ProductAvailabilityType $type): ProductAvailability + { + $this->type = $type; + + return $this; + } + + public function getStartAt(): \DateTimeImmutable + { + return $this->startAt; + } + + public function setStartAt(\DateTimeImmutable $startAt): ProductAvailability + { + $this->startAt = $startAt; + + return $this; + } + + public function getEndAt(): \DateTimeImmutable + { + return $this->endAt; + } + + public function setEndAt(\DateTimeImmutable $endAt): ProductAvailability + { + $this->endAt = $endAt; + + return $this; + } + + public static function productAvailabilityCreationByOwner(Product $product, \DateTimeImmutable $startAt, \DateTimeImmutable $endAt): ProductAvailability + { + $productAvailability = new self(); + + return $productAvailability + ->setProduct($product) + ->setType(ProductAvailabilityType::OWNER) + ->setStartAt($startAt) + ->setEndAt($endAt); + } +} diff --git a/src/Entity/ProductObjectTrait.php b/src/Entity/ProductObjectTrait.php new file mode 100644 index 0000000..b520eb7 --- /dev/null +++ b/src/Entity/ProductObjectTrait.php @@ -0,0 +1,104 @@ +age; + } + + public function setAge(?string $age): self + { + $this->age = $age; + + return $this; + } + + public function getDeposit(): ?int + { + return $this->deposit; + } + + public function setDeposit(?int $deposit): self + { + $this->deposit = $deposit; + + return $this; + } + + public function getCurrency(): ?string + { + return $this->currency; + } + + public function setCurrency(?string $currency): self + { + $this->currency = $currency; + + return $this; + } + + public function getPreferredLoanDuration(): ?string + { + return $this->preferredLoanDuration; + } + + public function setPreferredLoanDuration(?string $preferredLoanDuration): self + { + $this->preferredLoanDuration = $preferredLoanDuration; + + return $this; + } + + public function getDepositReal(): ?int + { + if ($this->deposit === null) { + return $this->deposit; + } + + return $this->deposit / 100; + } +} diff --git a/src/Entity/ProductServiceTrait.php b/src/Entity/ProductServiceTrait.php new file mode 100644 index 0000000..2e6d3b1 --- /dev/null +++ b/src/Entity/ProductServiceTrait.php @@ -0,0 +1,34 @@ +duration; + } + + public function setDuration(?string $duration): self + { + $this->duration = $duration; + + return $this; + } +} diff --git a/src/Entity/ServiceRequest.php b/src/Entity/ServiceRequest.php new file mode 100644 index 0000000..63fc9b8 --- /dev/null +++ b/src/Entity/ServiceRequest.php @@ -0,0 +1,334 @@ += startDate (can be the same day) + */ + #[ORM\Column(type: Types::DATETIME_IMMUTABLE)] + protected \DateTimeImmutable $endAt; + + /** + * Virtual field for the request service creation. This is the optional message + * sent by the borrower to the lender a the request service creation. It is stored + * in the conversation thread of the request service not the request service itself. + */ + protected ?string $message = null; + + /** + * @var Collection $messages + */ + #[ORM\OneToMany(mappedBy: 'serviceRequest', targetEntity: Message::class, cascade: ['persist', 'remove', 'detach'])] + #[ORM\OrderBy(['createdAt' => 'ASC'])] + private Collection $messages; + + public function __construct() + { + $this->messages = new ArrayCollection(); + } + + public function __toString() + { + return (string) $this->id; + } + + public function getId(): Uuid + { + return $this->id; + } + + public function setId(Uuid $id): ServiceRequest + { + $this->id = $id; + + return $this; + } + + public function getOwner(): User + { + return $this->owner; + } + + public function isOwner(?User $user): bool + { + return $this->owner === $user; + } + + public function setOwner(User $owner): ServiceRequest + { + $this->owner = $owner; + + return $this; + } + + public function getProduct(): Product + { + return $this->product; + } + + public function setProduct(Product $product): ServiceRequest + { + $this->product = $product; + + return $this; + } + + public function getRecipient(): User + { + return $this->recipient; + } + + public function isRecipient(User $user): bool + { + return $this->recipient === $user; + } + + public function setRecipient(User $recipient): ServiceRequest + { + $this->recipient = $recipient; + + return $this; + } + + public function getStatus(): ServiceRequestStatus + { + return $this->status; + } + + /** + * This function should never be called manually, except in tests. + * + * @internal + */ + public function setStatus(ServiceRequestStatus $status): ServiceRequest + { + $this->status = $status; + + return $this; + } + + /** + * This is needed for the workflow that works with strings, not enum. + * + * @internal + */ + public function getStatusRaw(): string + { + return $this->status->value; + } + + /** + * This function should never be called manually. + * + * @internal + */ + public function setStatusRaw(string $status): ServiceRequest + { + $this->status = ServiceRequestStatus::from($status); + + return $this; + } + + public function getStartAt(): \DateTimeImmutable + { + return $this->startAt; + } + + public function setStartAt(\DateTimeImmutable $startAt): ServiceRequest + { + $this->startAt = $startAt; + + return $this; + } + + public function getEndAt(): \DateTimeImmutable + { + return $this->endAt; + } + + public function setEndAt(\DateTimeImmutable $endAt): ServiceRequest + { + $this->endAt = $endAt; + + return $this; + } + + public function getMessage(): ?string + { + return $this->message; + } + + public function setMessage(?string $message): ServiceRequest + { + $this->message = $message; + + return $this; + } + + /** + * @return Collection + */ + public function getMessages(): Collection + { + return $this->messages; + } + + /** + * @param Collection $messages + */ + public function setMessages(Collection $messages): self + { + $this->messages = $messages; + + return $this; + } + + public function addMessage(Message $message): self + { + if (!$this->messages->contains($message)) { + $this->messages->add($message); + $message->setServiceRequest($this); + } + + return $this; + } + + public function removeMessage(Message $item): self + { + $this->messages->removeElement($item); + + return $this; + } + + public function messagesCount(): int + { + return $this->messages->count(); + } + + public function isLoan(): bool + { + return $this->product->getType()->isObject(); + } + + public function isService(): bool + { + return $this->product->getType()->isService(); + } + + // end of basic 'etters ———————————————————————————————————————————————————— + + public function isOwnerOrRecipient(User $user): bool + { + return $this->owner === $user || $this->recipient === $user; + } + + public function hasUnreadMessages(User $user): bool + { + $messages = $this->messages->filter( + fn (Message $message) => $this->isOwner($user) ? + !$message->isOwnerRead() : + !$message->isRecipientRead()); + + return !$messages->isEmpty(); + } + + /** + * The the other user involved in the service request. + */ + public function getOtherUser(?User $actor): User + { + return $this->isOwner($actor) ? $this->getRecipient() : $this->getOwner(); + } + + /** + * Get the finalized date from the end date. We consider a service request is + * finished the day just after the end date to avoid overlap problems. + */ + public function getFinalizedAt(): \DateTimeImmutable + { + return $this->getEndAt()->modify('tomorrow midnight'); + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100755 index 0000000..132004d --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,897 @@ + + */ + #[ORM\Column] + private array $roles = []; + + /** + * The hashed password. + */ + #[ORM\Column(nullable: true)] + private ?string $password = null; + + /** + * The password before it is encrypted. + */ + #[Assert\Length(min: UserManager::PASWWORD_MIN_LENGTH, max: UserManager::PASWWORD_MAX_LENGTH, groups: [AccountCreateStep2FormType::class, ChangePasswordFormType::class, 'Default'])] + // #[Assert\NotCompromisedPassword] // enable to check the password with the https://haveibeenpwned.com/ service + #[Assert\NotBlank(groups: [AccountCreateStep2FormType::class, ChangePasswordFormType::class])] + private ?string $plainPassword = null; + + #[SecurityAssert\UserPassword(groups: [ChangePasswordFormType::class])] + private ?string $oldPassword = null; + + /** + * Last login date of the user, null if has never logged in. The email confirmation + * does not count as a valid login. + */ + #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] + private ?\DateTimeInterface $loginAt = null; + + /** + * Tells if the user wants to receive sms notifications. + */ + #[ORM\Column(type: 'boolean', nullable: true)] + #[Assert\Type('bool')] + private bool $smsNotifications = false; + + /** + * If it is a place, it tells its schedules. + */ + #[ORM\Column(length: self::SCHEDULE_LENGTH, nullable: true)] + #[Assert\Length(max: self::SCHEDULE_LENGTH)] + private ?string $schedule = null; + + /** + * User's favorite category. + */ + #[ORM\ManyToOne(targetEntity: Category::class)] + #[ORM\JoinColumn(referencedColumnName: 'id')] + private ?Category $category = null; + + /** + * User's description. + */ + #[ORM\Column(type: 'string', nullable: true, )] + private ?string $description = null; + + /** + * Tells if the user in on vacation. + */ + #[ORM\Column(type: 'boolean')] + #[Assert\Type('bool')] + private bool $vacationMode = false; + + /** + * Main address of the user/place. + */ + #[ORM\ManyToOne(targetEntity: Address::class, cascade: ['persist'])] + #[ORM\JoinColumn(referencedColumnName: 'id')] + private ?Address $address = null; + + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'user', targetEntity: UserGroup::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + private Collection $userGroups; + + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'user', targetEntity: Payment::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + private Collection $payments; + + #[Assert\IsTrue(groups: [AccountCreateStep2FormType::class])] + public bool $gdpr = true; + + /** + * Local cache to store groups (extracted from related userGroups). + * + * @var Collection|null + */ + private ?Collection $groups = null; + + public function __construct() + { + $this->userGroups = new ArrayCollection(); + $this->payments = new ArrayCollection(); + } + + public function __toString(): string + { + return $this->email; + } + + public function getId(): Uuid + { + return $this->id; + } + + public function setId(Uuid $uuid): self + { + $this->id = $uuid; + + return $this; + } + + public function getType(): ?UserType + { + return $this->type; + } + + public function setType(?UserType $type): User + { + $this->type = $type; + + return $this; + } + + public function getEmail(): string + { + return $this->email; + } + + public function setEmail(string $email): self + { + $this->email = $email; + + return $this; + } + + public function isEmailConfirmed(): bool + { + return $this->emailConfirmed; + } + + public function setEmailConfirmed(bool $emailConfirmed): User + { + $this->emailConfirmed = $emailConfirmed; + + return $this; + } + + public function getLastname(): ?string + { + return $this->lastname; + } + + public function setLastname(?string $lastname): User + { + $this->lastname = $lastname; + + return $this; + } + + public function getFirstname(): ?string + { + return $this->firstname; + } + + public function setFirstname(?string $firstname): User + { + $this->firstname = $firstname; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): User + { + $this->name = $name; + + return $this; + } + + public function getPhoneNumber(): ?string + { + return $this->phoneNumber; + } + + public function setPhoneNumber(?string $phoneNumber): void + { + $this->phoneNumber = $phoneNumber; + } + + /** + * Transforms the user phone number string into a phone object. + */ + public function getPhone(): ?PhoneNumber + { + if (u($this->phoneNumber)->isEmpty()) { + return null; + } + \Webmozart\Assert\Assert::notEmpty($this->phoneNumber); + + try { + return PhoneNumberUtil::getInstance()->parse($this->phoneNumber, PhoneNumberUtil::UNKNOWN_REGION); + } catch (\Exception) { + // wrong data in the database, then ignore and return null so a new number can be put + return null; + } + } + + public function setPhone(?PhoneNumber $phone): void + { + $this->phone = $phone; + } + + public function getAvatar(): ?string + { + return $this->avatar; + } + + public function setAvatar(?string $avatar): self + { + $this->avatar = $avatar; + + return $this; + } + + public function isEnabled(): bool + { + return $this->enabled; + } + + public function setEnabled(bool $enabled): self + { + $this->enabled = $enabled; + + return $this; + } + + public function isMainAdminAccount(): bool + { + return $this->mainAdminAccount; + } + + public function setMainAdminAccount(bool $mainAdminAccount): self + { + $this->mainAdminAccount = $mainAdminAccount; + + return $this; + } + + public function isDevAccount(): bool + { + return $this->devAccount; + } + + public function setDevAccount(bool $devAccount): User + { + $this->devAccount = $devAccount; + + return $this; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return $this->email; + } + + /** + * @see UserInterface + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = self::ROLE_USER; + + // add specific group roles + foreach ($this->userGroups as $userGroup) { + if ($userGroup->getMembership()->isAdmin()) { + $roles[] = self::ROLE_GROUP_ADMIN; + } + } + + return array_unique($roles); + } + + /** + * @param array $roles + */ + public function setRoles(array $roles): self + { + $this->roles = $roles; + + return $this; + } + + /** + * @see PasswordAuthenticatedUserInterface + */ + public function getPassword(): string + { + return (string) $this->password; + } + + public function setPassword(?string $password): self + { + $this->password = $password; + + return $this; + } + + public function getPlainPassword(): ?string + { + return $this->plainPassword; + } + + public function setPlainPassword(?string $plainPassword): self + { + $this->plainPassword = $plainPassword; + + return $this; + } + + public function getOldPassword(): ?string + { + return $this->oldPassword; + } + + public function setOldPassword(?string $oldPassword): void + { + $this->oldPassword = $oldPassword; + } + + public function getLoginAt(): ?\DateTimeInterface + { + return $this->loginAt; + } + + public function setLoginAt(?\DateTimeInterface $loginAt): User + { + $this->loginAt = $loginAt; + + return $this; + } + + public function getSmsNotifications(): bool + { + return $this->smsNotifications; + } + + public function setSmsNotifications(bool $smsNotifications): self + { + $this->smsNotifications = $smsNotifications; + + return $this; + } + + public function canBeNotifiedBySms(): bool + { + return $this->getSmsNotifications() + && !u($this->phoneNumber)->isEmpty(); + } + + public function getSchedule(): ?string + { + return $this->schedule; + } + + public function setSchedule(?string $schedule): void + { + $this->schedule = $schedule; + } + + public function getCategory(): ?Category + { + return $this->category; + } + + public function setCategory(?Category $category): void + { + $this->category = $category; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): void + { + $this->description = $description; + } + + public function getVacationMode(): bool + { + return $this->vacationMode; + } + + public function isInVacation(): bool + { + return $this->vacationMode; + } + + public function setVacationMode(bool $vacationMode): void + { + $this->vacationMode = $vacationMode; + } + + public function switchVacationMode(bool $vacationMode): void + { + $this->vacationMode = !$vacationMode; + } + + /** + * @see UserInterface + */ + public function eraseCredentials(): void + { + // If you store any temporary, sensitive data on the user, clear it here + $this->plainPassword = null; + } + + public function isAdmin(): bool + { + return $this->type === UserType::ADMIN; + } + + public function isPlace(): bool + { + return $this->type === UserType::PLACE; + } + + public function setStep2Defaults(): self + { + $this->type = UserType::USER; + + return $this; + } + + public function getImage(): ?string + { + return $this->getAvatar(); + } + + public function getAddress(): ?Address + { + return $this->address; + } + + public function hasAddress(): bool + { + return $this->address !== null; + } + + public function setAddress(?Address $address): User + { + $this->address = $address; + + return $this; + } + + /** + * @return Collection + */ + public function getUserGroups(): Collection + { + return $this->userGroups; + } + + /** + * @return Collection + */ + public function getUserGroupsConfirmed(): Collection + { + /** @var Collection $collection */ + $collection = $this->userGroups->filter(fn (UserGroup $userGroup) => !$userGroup->getMembership()->isInvited()); + + return $collection; + } + + /** + * @return array + */ + public function getUserGroupsIds(): array + { + return $this->getUserGroupsConfirmed()->map(fn (UserGroup $userGroup) => (string) $userGroup->getGroup()->getId())->toArray(); + } + + public function addUserGroup(UserGroup $userGroup): self + { + if (!$this->userGroups->contains($userGroup)) { + $this->userGroups->add($userGroup); + $userGroup->setUser($this); + } + + return $this; + } + + public function removeUserGroup(UserGroup $userGroup): self + { + $this->userGroups->removeElement($userGroup); + + return $this; + } + + /** + * @return Collection + */ + public function getPayments(): Collection + { + return $this->payments; + } + + /** + * @param Collection $payments + */ + public function setPayments(Collection $payments): User + { + $this->payments = $payments; + + return $this; + } + + // —— end of basic 'etters ————————————————————————————————————————————————— + + public function promoteToAdmin(): self + { + $this->setRoles([self::ROLE_ADMIN]); + + return $this; + } + + public function getDisplayName(): string + { + if ($this->isPlace()) { + $shortName = $this->getName(); + } else { + $shortName = $this->getFirstname(); + } + + return (string) $shortName; + } + + /** + * @return class-string + */ + public function getAdminCrudClass(): string + { + return match ($this->type) { + UserType::USER => UserCrudController::class, + UserType::ADMIN => AdministratorCrudController::class, + UserType::PLACE => PlaceCrudController::class, + default => throw new \LogicException('No type assigned to user yet.') + }; + } + + /** + * Get the list of groups the user belong to as a collection (with local cache). + * + * @return Collection + */ + public function getMyGroups(): Collection + { + if ($this->groups !== null) { + return $this->groups; + } + + $this->groups = new ArrayCollection( + array_map(static fn (UserGroup $userGroup) => $userGroup->getGroup(), $this->userGroups->toArray()) + ); + + return $this->groups; + } + + /** + * Get the groups only where the user has the group admin role. + * + * @return Collection + */ + public function getMyGroupsAsAdmin(): Collection + { + $adminUserGroups = $this->userGroups->filter( + static fn (UserGroup $userGroup) => $userGroup->getMembership()->isAdmin() || $userGroup->isMainAdminAccount() + ); + + return new ArrayCollection( + array_map(static fn (UserGroup $userGroup) => $userGroup->getGroup(), $adminUserGroups->toArray()) + ); + } + + /** + * Get the groups only where the user has invitations. + * + * @return Collection + */ + public function getMyUserGroupsAsInvited(): Collection + { + /** @var Collection $collection */ + $collection = $this->userGroups->filter( + static fn (UserGroup $userGroup) => $userGroup->getMembership()->isInvited() + ); + + return $collection; + } + + /** + * Get the groups only where the user is confirmed (member or admin). + * + * @return Collection + */ + public function getMyUserGroupsAsConfirmed(): Collection + { + /** @var Collection $collection */ + $collection = $this->userGroups->filter( + static fn (UserGroup $userGroup) => $userGroup->getMembership()->isConfirmed() + ); + + return $collection; + } + + /** + * Get the groups only where the user has invitations. + * + * @return Collection + */ + public function getMyGroupsAsInvited(): Collection + { + return new ArrayCollection( + array_map(static fn (UserGroup $userGroup) => $userGroup->getGroup(), $this->getMyUserGroupsAsInvited()->toArray()) + ); + } + + /** + * The invitation status is excluded because, we are only member of the group + * once the invitation is accepted. We consider we are also a member even we + * are an admin of the group. + */ + public function isMemberOf(Group $group): bool + { + $notInvited = $this->userGroups->filter( + fn (UserGroup $userGroup) => $userGroup->getGroup() === $group && !$userGroup->getMembership()->isInvited() + ); + + return !$notInvited->isEmpty(); + } + + /** + * Tells if the user has already an association with the group whatever the + * membership status is. + */ + public function hasLink(Group $group): bool + { + return $this->getMyGroups()->contains($group); + } + + /** + * Return the membership for a given group if it exists for the user, null otherwise. + * We can safely use the first() function here. Because of Doctrine constraints, + * it's impossible to have 2 records for the same group and user. + * + * @see UserGroup + */ + public function getGroupMembership(Group $group): ?UserGroup + { + /** @var Collection $contextUserGroup */ + $contextUserGroup = $this->userGroups->filter( + static fn (UserGroup $userGroup) => $userGroup->getGroup() === $group + ); + + return $contextUserGroup->isEmpty() ? null : $contextUserGroup->first(); + } + + public function isGroupAdmin(Group $group): bool + { + $groupAdmin = $group->getUserGroups()->filter( + fn (UserGroup $userGroup) => $userGroup->getUser()->getId() === $this->getId() && $userGroup->getMembership()->isAdmin() + ); + + return !$groupAdmin->isEmpty(); + } + + public function isIndexable(): bool + { + return !$this->isInVacation(); + } + + public function deleteAvatar(): self + { + $this->avatar = null; + + return $this; + } + + public function changePhoneNumber(?PhoneNumber $phone): self + { + if ($phone === null) { + $this->setPhoneNumber(null); + } else { + $this->setPhoneNumber('+'.$phone->getCountryCode().$phone->getNationalNumber()); + } + + return $this; + } +} diff --git a/src/Entity/UserConfirmationTrait.php b/src/Entity/UserConfirmationTrait.php new file mode 100644 index 0000000..24d7053 --- /dev/null +++ b/src/Entity/UserConfirmationTrait.php @@ -0,0 +1,80 @@ +confirmationToken; + } + + public function setConfirmationToken(?string $confirmationToken): self + { + $this->confirmationToken = $confirmationToken; + + return $this; + } + + public function getConfirmationExpiresAt(): ?\DateTimeInterface + { + return $this->confirmationExpiresAt; + } + + public function setConfirmationExpiresAt(?\DateTimeInterface $confirmationExpiresAt): self + { + $this->confirmationExpiresAt = $confirmationExpiresAt; + + return $this; + } + + /** + * Test if the token is still valid. + */ + public function isConfirmationTokenExpired(\DateTimeInterface $now): bool + { + return $now > $this->getConfirmationExpiresAt(); + } + + /** + * Reset all properties after a successful confirmation. + */ + public function resetConfirmation(): void + { + $this->setConfirmationToken(null); + $this->setConfirmationExpiresAt(null); + } + + /** + * Mark the email as confirmed, user can now login. + */ + public function confirmEmail(): void + { + $this->setEmailConfirmed(true); + } +} diff --git a/src/Entity/UserGroup.php b/src/Entity/UserGroup.php new file mode 100644 index 0000000..8400882 --- /dev/null +++ b/src/Entity/UserGroup.php @@ -0,0 +1,216 @@ +id; + } + + public function setId(Uuid $uuid): self + { + $this->id = $uuid; + + return $this; + } + + public function getUser(): User + { + return $this->user; + } + + public function setUser(User $user): self + { + $this->user = $user; + + return $this; + } + + public function getGroup(): Group + { + return $this->group; + } + + public function setGroup(Group $group): self + { + $this->group = $group; + + return $this; + } + + public function getMembership(): UserMembership + { + return $this->membership; + } + + public function setMembership(UserMembership $membership): UserGroup + { + $this->membership = $membership; + + return $this; + } + + public function isMainAdminAccount(): bool + { + return $this->mainAdminAccount; + } + + public function setMainAdminAccount(bool $mainAdminAccount): self + { + $this->mainAdminAccount = $mainAdminAccount; + + return $this; + } + + public function getStartAt(): ?\DateTimeImmutable + { + return $this->startAt; + } + + public function setStartAt(?\DateTimeImmutable $startAt): UserGroup + { + $this->startAt = $startAt; + + return $this; + } + + public function getEndAt(): ?\DateTimeImmutable + { + return $this->endAt; + } + + public function setEndAt(?\DateTimeImmutable $endAt): UserGroup + { + $this->endAt = $endAt; + + return $this; + } + + public function getPayedAt(): ?\DateTimeImmutable + { + return $this->payedAt; + } + + public function setPayedAt(?\DateTimeImmutable $payedAt): UserGroup + { + $this->payedAt = $payedAt; + + return $this; + } + + /** -- End of basic getters and setters ----------------------------------------------*/ + + /** + * Don't remove the admin role if set. + */ + public function setMember(): self + { + if (!$this->membership->isAdmin()) { + $this->membership = UserMembership::MEMBER; + } + + return $this; + } + + public static function newUserGroup(User $user, Group $group): UserGroup + { + return (new self()) + ->setUser($user) + ->setMembership(UserMembership::ADMIN) + ->setMainAdminAccount(true) + ->setGroup($group); + } + + /** + * Return the number of days the mmebership will expires in relative days without + * taking take of the hour. + */ + public function expiresIn(): ?int + { + $today = Carbon::today(); + if ($this->endAt === null || $this->endAt < $today) { + return null; + } + + $endAt = new Carbon($this->endAt); + + return $today->diffInDays($endAt); + } +} diff --git a/src/Entity/UserLostPasswordTrait.php b/src/Entity/UserLostPasswordTrait.php new file mode 100644 index 0000000..ad83643 --- /dev/null +++ b/src/Entity/UserLostPasswordTrait.php @@ -0,0 +1,72 @@ +lostPasswordToken; + } + + public function setLostPasswordToken(?string $lostPasswordToken): self + { + $this->lostPasswordToken = $lostPasswordToken; + + return $this; + } + + public function getLostPasswordExpiresAt(): ?\DateTimeInterface + { + return $this->lostPasswordExpiresAt; + } + + public function setLostPasswordExpiresAt(?\DateTimeInterface $lostPasswordExpiresAt): self + { + $this->lostPasswordExpiresAt = $lostPasswordExpiresAt; + + return $this; + } + + /** + * Reset all properties after a successful reset. + */ + public function resetLostPawword(): void + { + $this->setLostPasswordToken(null); + $this->setLostPasswordExpiresAt(null); + } + + /** + * Test if the token is still valid. + */ + public function isLostPasswordTokenExpired(\DateTimeInterface $now): bool + { + return $now > $this->getLostPasswordExpiresAt(); + } +} diff --git a/src/Enum/AsArrayTrait.php b/src/Enum/AsArrayTrait.php new file mode 100644 index 0000000..bca6a3b --- /dev/null +++ b/src/Enum/AsArrayTrait.php @@ -0,0 +1,20 @@ + + */ + public static function getAsArray(): array + { + return array_reduce( + self::cases(), + static fn (array $choices, self $type) => $choices + [$type->name => $type->value], + [], + ); + } +} diff --git a/src/Enum/ConfigurationType.php b/src/Enum/ConfigurationType.php new file mode 100644 index 0000000..5cf58f8 --- /dev/null +++ b/src/Enum/ConfigurationType.php @@ -0,0 +1,10 @@ +isYearly() || $this->isMonthly(); + } + + public function getEndAtInterval(): string + { + return match ($this) { + self::YEARLY => '+1 year midnight', + self::MONTHLY => '+1 month midnight', + self::ONESHOT => '', + }; + } +} diff --git a/src/Enum/Group/GroupType.php b/src/Enum/Group/GroupType.php new file mode 100644 index 0000000..7989422 --- /dev/null +++ b/src/Enum/Group/GroupType.php @@ -0,0 +1,27 @@ +getI18nPrefix().'.'.$this->value; + } +} diff --git a/src/Enum/Group/UserMembership.php b/src/Enum/Group/UserMembership.php new file mode 100644 index 0000000..bdcc536 --- /dev/null +++ b/src/Enum/Group/UserMembership.php @@ -0,0 +1,36 @@ +isMember() || $this->isAdmin(); + } +} diff --git a/src/Enum/Menu/LinkType.php b/src/Enum/Menu/LinkType.php new file mode 100644 index 0000000..fbae90e --- /dev/null +++ b/src/Enum/Menu/LinkType.php @@ -0,0 +1,16 @@ +isActive(); + } +} diff --git a/src/Enum/Product/ProductType.php b/src/Enum/Product/ProductType.php new file mode 100644 index 0000000..de86d71 --- /dev/null +++ b/src/Enum/Product/ProductType.php @@ -0,0 +1,25 @@ +isNew() || $this->isToConfirm() || $this->isConfirmed(); + } +} diff --git a/src/Enum/ServiceRequest/ServiceRequestStatusTransition.php b/src/Enum/ServiceRequest/ServiceRequestStatusTransition.php new file mode 100644 index 0000000..7a789bc --- /dev/null +++ b/src/Enum/ServiceRequest/ServiceRequestStatusTransition.php @@ -0,0 +1,20 @@ + + */ + public static function getForFront(): array + { + return [ + self::USER->name => self::USER->value, + self::PLACE->name => self::PLACE->value, + ]; + } +} diff --git a/src/Exception/Group/GroupNotFoundException.php b/src/Exception/Group/GroupNotFoundException.php new file mode 100644 index 0000000..7529b98 --- /dev/null +++ b/src/Exception/Group/GroupNotFoundException.php @@ -0,0 +1,15 @@ +id = $id; + parent::__construct('The email confirmation token is expired for this user'); + } +} diff --git a/src/Exception/UserLostPasswordTokenExpiredException.php b/src/Exception/UserLostPasswordTokenExpiredException.php new file mode 100644 index 0000000..c0c2b20 --- /dev/null +++ b/src/Exception/UserLostPasswordTokenExpiredException.php @@ -0,0 +1,16 @@ +write($fileName, $file->getContent()); + }; + } + + /** + * Callack used when deleting a file. + */ + public function getUploadDeleteCallback(FilesystemOperator $storage): callable + { + return static function (File $file) use ($storage) { + $storage->delete($file->getFilename()); + }; + } +} diff --git a/src/Flysystem/MediaManager.php b/src/Flysystem/MediaManager.php new file mode 100644 index 0000000..edfe889 --- /dev/null +++ b/src/Flysystem/MediaManager.php @@ -0,0 +1,80 @@ + $uploadImagesAllowedExtensions + */ + public function __construct( + #[Autowire('%upload_images_allowed_extensions%')] + public readonly array $uploadImagesAllowedExtensions, + #[Autowire('%upload_maxsize_by_file%')] + public readonly int $uploadMaxsizeByFile, + ) { + $this->uploadImagesAllowedExtensionsMsg = implode(', ', $uploadImagesAllowedExtensions); + } + + /** + * To validate a property containing a single image. + * + * @see https://symfony.com/doc/current/reference/constraints/File.html + */ + public function getFileConstraints(?string $validationGroup = null): AppAssert\File + { + return new AppAssert\File( + maxSize: $this->uploadMaxsizeByFile.'mi', + groups: $validationGroup !== null ? [$validationGroup] : null, + extensions: $this->uploadImagesAllowedExtensions, + extensionsMessage: 'validator.extensions_message', + ); + } + + /** + * To validate a property containing a collection of images (as an array). + * + * @see https://symfony.com/doc/current/reference/constraints/All.html + */ + public function getImageArrayConstraints(?string $validationGroup = null): Assert\All + { + return new Assert\All([ + $this->getFileConstraints($validationGroup), + ]); + } + + /** + * Help message that displays the allowed extension and the maximum size by + * image. + */ + public function getHelpMessage(): TranslatableMessage + { + return + t('images.help', [ + '%extensions%' => $this->uploadImagesAllowedExtensionsMsg, + '%upload_maxsize_by_file%' => $this->uploadMaxsizeByFile, + ], DashboardController::DOMAIN); + } +} diff --git a/src/Form/Type/Admin/ParametersFormType.php b/src/Form/Type/Admin/ParametersFormType.php new file mode 100755 index 0000000..91ae35a --- /dev/null +++ b/src/Form/Type/Admin/ParametersFormType.php @@ -0,0 +1,96 @@ +add('notificationsSenderEmail', EmailType::class, [ + 'label' => 'parameter.mail', + 'label_attr' => ['class' => 'col-sm-2 col-form-label'], + ]) + ->add('notificationsSenderName', TextType::class, [ + 'label' => 'parameter.name', + ]) + + ->add('contactFormEnabled', CheckboxType::class, [ + 'label' => 'parameter.formVisibility', + 'label_attr' => [ + 'class' => 'checkbox-inline checkbox-switch', + ], + ]) + ->add('contactFormEmail', EmailType::class, [ + 'label' => 'parameter.receptionEmail', + ]) + + ->add('groupsEnabled', CheckboxType::class, [ + 'label' => 'parameter.groups', + 'label_attr' => [ + 'class' => 'checkbox-inline checkbox-switch', + ], + ]) + + ->add('groupsCreationMode', ChoiceType::class, [ + 'label' => 'parameter.groupsCreation', + 'expanded' => true, + 'choices' => [ + 'parameter.groupsCreationForAll' => ParametersFormCommand::ALL, + 'parameter.groupsCreationOnlyForAdmin' => ParametersFormCommand::ONLY_ADMIN, + ], + 'label_attr' => [ + 'class' => 'radio-inline', + ], + ]) + + ->add('groupsPaying', CheckboxType::class, [ + 'label' => 'parameter.paidGroupsCreation', + 'label_attr' => [ + 'class' => 'checkbox-inline checkbox-switch', + ], + ]) + + ->add('confidentialityConversationAdminAccess', CheckboxType::class, [ + 'label' => 'parameter.conversationsVisibility', + 'label_attr' => [ + 'class' => 'checkbox-inline checkbox-switch', + ], + ]) + + ->add('submit', SubmitType::class, [ + 'label' => 'parameter.save', + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'csrf_protection' => true, + 'data_class' => ParametersFormCommand::class, + 'attr' => [ + 'novalidate' => 'novalidate', // test constraintes HTML + ], + 'translation_domain' => DashboardController::DOMAIN, + ]); + } +} diff --git a/src/Form/Type/Group/CreateGroupFormType.php b/src/Form/Type/Group/CreateGroupFormType.php new file mode 100644 index 0000000..565827a --- /dev/null +++ b/src/Form/Type/Group/CreateGroupFormType.php @@ -0,0 +1,78 @@ +security->getUser(); + + $builder + ->add('name', TextType::class, [ + 'label' => 'templates.pages.group.create.form.name', + 'label_attr' => ['class' => 'fs-6 text-black'], + 'attr' => [ + 'class' => 'form-control-sm', + ], + ]) + ->add('type', EnumType::class, [ + 'class' => GroupType::class, + 'label' => 'templates.pages.group.create.form.type', + 'label_attr' => ['class' => 'fs-6 text-black'], + 'expanded' => true, + 'choice_label' => 'transKey', + ]) + ->add('membership', EnumType::class, [ + 'class' => GroupMembership::class, + 'label' => 'templates.pages.group.create.form.membership', + 'label_attr' => ['class' => 'fs-6 text-black'], + 'expanded' => true, + ]) + ->add('submit', SubmitType::class, [ + 'label' => 'templates.pages.group.create.form.submit', + 'attr' => ['class' => 'btn btn-primary btn-sm d-grid col-12 my-5'], + ]); + + $myGroups = $user->getMyGroupsAsAdmin(); + if (!$myGroups->isEmpty()) { + $builder + ->add('parent', EntityType::class, [ + 'class' => Group::class, + 'choices' => $myGroups, + 'label' => 'templates.pages.group.create.form.subgroup', + 'label_attr' => ['class' => 'fs-6 text-black'], + 'required' => false, + ]); + } + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Group::class, + 'validation_groups' => self::class, + ]); + } +} diff --git a/src/Form/Type/Product/AbstractProductCategorySelectFormType.php b/src/Form/Type/Product/AbstractProductCategorySelectFormType.php new file mode 100644 index 0000000..6426b54 --- /dev/null +++ b/src/Form/Type/Product/AbstractProductCategorySelectFormType.php @@ -0,0 +1,52 @@ +security->getUser(); + Assert::isInstanceOf($user, User::class); + + return $builder + ->add('category', EntityType::class, [ + 'class' => Category::class, + 'query_builder' => fn (CategoryRepository $er) => $er->getHierarchy($this->getProductType(), $user), + 'required' => false, + 'placeholder' => 'select_placeholder', + 'label' => false, + ]) + ->add('submit', SubmitType::class) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'csrf_protection' => false, + ]); + } +} diff --git a/src/Form/Type/Product/AbstractProductFormType.php b/src/Form/Type/Product/AbstractProductFormType.php new file mode 100644 index 0000000..52dd3b0 --- /dev/null +++ b/src/Form/Type/Product/AbstractProductFormType.php @@ -0,0 +1,138 @@ + $options + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $i18nPrefix = $this->getI18nPrefix(); + /** @var User $user */ + $user = $this->security->getUser(); + + $builder + ->add('category', EntityType::class, [ + 'class' => Category::class, + 'query_builder' => fn (CategoryRepository $er) => $er->getHierarchy($this->getType()), + 'label' => 'product.form.category', + 'label_attr' => ['class' => 'text-black fs-6 fw-normal'], + 'choice_label' => 'getNameWithIndent', + 'empty_data' => Category::getForEmptyData(), // trick to avoid type error when submitting a blank value in tests + ]) + ->add('name', TextType::class, [ + 'label_attr' => ['class' => 'text-black fs-6 fw-normal'], + 'empty_data' => '', + ]) + ->add('description', TextareaType::class, [ + 'label' => 'product.form.description', + 'label_attr' => ['class' => 'text-black fs-6 fw-normal'], + 'attr' => ['style' => 'height: 120px'], + ]) + ->add('images', FileType::class, [ + 'label' => 'product.form.images', + 'multiple' => true, + 'mapped' => false, + 'required' => false, + 'constraints' => [ + new Assert\Count( + max: MediaManager::MAX_PHOTO_COUNT, + groups: [self::class] + ), + $this->mediaManager->getImageArrayConstraints(self::class), + ], + 'help' => 'product.form.upload_infos', + ]) + ->add('submit', SubmitType::class, [ + 'label' => 'product.form.submit', + 'attr' => ['class' => 'btn-sm btn-primary'], + ]); + + // only if the user has groups + if (!$user->getUserGroupsConfirmed()->isEmpty()) { + $builder + ->add('visibility', EnumType::class, [ + 'class' => ProductVisibility::class, + 'label' => 'product.form.visibility', + 'expanded' => true, + 'label_attr' => [ + 'class' => 'radio-inline text-black fs-6 fw-normal', + ], + // check if we can do simpler (@see productvisibility_controller.js) + 'choice_attr' => [ + 0 => [ + 'data-productvisibility-target' => ProductVisibility::PUBLIC->value, + 'data-action' => 'click->productvisibility#hideGroups', + ], + 1 => [ + 'data-productvisibility-target' => ProductVisibility::RESTRICTED->value, + 'data-action' => 'click->productvisibility#showGroups', + ], + ], + ]) + ->add('groups', EntityType::class, [ + 'class' => Group::class, + 'query_builder' => $this->groupRepository->getUserGroups($user), + 'label' => $i18nPrefix.'.form.groups', + 'expanded' => true, + 'multiple' => true, + 'required' => false, + ]); + } + + // remove all group associations if public to avoid side effects + $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event): void { + /** @var Product $product */ + $product = $event->getData(); + if ($product->getVisibility()->isPublic()) { + $product->removeGroups(); + } + }); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'validation_groups' => self::class, + ]); + } +} diff --git a/src/Form/Type/Product/CreateProductAvailabilityType.php b/src/Form/Type/Product/CreateProductAvailabilityType.php new file mode 100644 index 0000000..c204a3b --- /dev/null +++ b/src/Form/Type/Product/CreateProductAvailabilityType.php @@ -0,0 +1,45 @@ +add('startAt', DateType::class, [ + 'label' => $this->getI18nPrefix().'.startAt', + 'label_attr' => ['class' => ''], + 'attr' => [ + 'class' => '', + 'placeholder' => '', + ], + 'input' => 'datetime_immutable', + 'widget' => 'single_text', + ]) + ->add('endAt', DateType::class, [ + 'label' => $this->getI18nPrefix().'.endAt', + 'label_attr' => ['class' => ''], + 'attr' => [ + 'class' => '', + 'placeholder' => '', + ], + 'input' => 'datetime_immutable', + 'widget' => 'single_text', + ]) + ->add('submit', SubmitType::class, [ + 'label' => $this->getI18nPrefix().'.submit', + ]) + ; + } +} diff --git a/src/Form/Type/Product/GroupSelectFormType.php b/src/Form/Type/Product/GroupSelectFormType.php new file mode 100644 index 0000000..95b7dfb --- /dev/null +++ b/src/Form/Type/Product/GroupSelectFormType.php @@ -0,0 +1,28 @@ +setMethod('GET') + ->add('q', SearchType::class, [ + 'label' => false, + 'required' => false, + ]) + ->add('submit', SubmitType::class, [ + 'label' => 'templates.pages.user.group.list.form.submit', + 'label_html' => true, + 'attr' => ['class' => 'search-input-button input-group-text'], + ]); + } +} diff --git a/src/Form/Type/Product/ObjectCategorySelectFormType.php b/src/Form/Type/Product/ObjectCategorySelectFormType.php new file mode 100644 index 0000000..701e92f --- /dev/null +++ b/src/Form/Type/Product/ObjectCategorySelectFormType.php @@ -0,0 +1,15 @@ +add('age', TextType::class, [ + 'label' => 'object.form.age', + 'label_attr' => ['class' => 'text-black fs-6 fw-normal'], + 'required' => false, + ]) + ->add('deposit', MoneyType::class, [ + 'label' => 'object.form.deposit', + 'divisor' => 100, + 'label_attr' => ['class' => 'text-black fs-6 fw-normal'], + 'required' => false, + ]) + ->add('preferredLoanDuration', TextType::class, [ + 'label' => 'object.form.preferredLoanDuration', + 'label_attr' => ['class' => 'text-black fs-6 fw-normal'], + 'required' => false, + ]); + } + + public function getType(): ProductType + { + return ProductType::OBJECT; + } + + public function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + $resolver->setDefaults([ + 'data_class' => Product::class, + 'attr' => [ + 'novalidate' => 'novalidate', + ], + ]); + } +} diff --git a/src/Form/Type/Product/SearchFormType.php b/src/Form/Type/Product/SearchFormType.php new file mode 100644 index 0000000..8d70f1a --- /dev/null +++ b/src/Form/Type/Product/SearchFormType.php @@ -0,0 +1,123 @@ + $options + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $distanceChoicesAttr = array_flip(self::DISTANCE_CHOICES); + $distanceChoicesAttr = array_map(static fn () => ['class' => 'form-check-input border border-2 mx-auto'], $distanceChoicesAttr); + $i18nPrefix = $this->getI18nPrefix(); + + $builder + ->add('q', TextType::class, [ + 'empty_data' => '', + 'attr' => [ + 'placeholder' => $this->translator->trans($i18nPrefix.'.q.placeholder'), + ], + ]) + ->add('category', EntityType::class, [ + 'class' => Category::class, + 'required' => false, + 'placeholder' => $this->translator->trans($i18nPrefix.'.category.placeholder'), + 'query_builder' => fn (CategoryRepository $er) => $er->getHierarchy(), + 'group_by' => fn (Category $category) => $this->translator->trans($i18nPrefix.'.'.$category->getType()->value), + 'choice_label' => 'getNameWithIndent', + ]) + ->add('place', EntityType::class, [ + 'required' => false, + 'placeholder' => $this->translator->trans($i18nPrefix.'.place.placeholder'), + 'class' => User::class, + 'query_builder' => fn (UserRepository $userRepository) => $userRepository->getPlacesQueryBuilder(), + 'group_by' => fn (User $user) => u($user->getAddress()?->getLocality())->lower()->title()->toString(), + 'choice_label' => fn (User $user) => $user->getDisplayName(), + ]) + ->add('city', TextType::class) + ->add('distance', ChoiceType::class, [ + 'required' => false, + 'placeholder' => false, + 'expanded' => true, + 'choices' => array_combine(self::DISTANCE_CHOICES, self::DISTANCE_CHOICES), + 'choice_label' => fn (string $choice) => $choice.' km', + 'choice_attr' => $distanceChoicesAttr, + 'label' => $i18nPrefix.'.proximity.label', + ]) + ->add('submit', SubmitType::class, [ + 'label' => $i18nPrefix.'.submit.label', + 'attr' => ['class' => 'btn-sm btn-primary'], + ]); + + $builder->get('city') + ->addModelTransformer(new CallbackTransformer( + function ($city): string { + return ''; + }, + function ($city): ?Address { + // transform the string back to an address + /** @var string $city */ + if (u($city)->isEmpty()) { + return null; + } + + return $this->geoProvider->getAddress($city); + } + )) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'method' => 'GET', + 'csrf_protection' => false, + 'attr' => [ + 'novalidate' => 'novalidate', + 'data-search-target' => 'form', + ], + 'data_class' => Search::class, + ]); + } + + public function getBlockPrefix(): string + { + return 'p'; + } +} diff --git a/src/Form/Type/Product/ServiceCategorySelectFormType.php b/src/Form/Type/Product/ServiceCategorySelectFormType.php new file mode 100644 index 0000000..6fe1c1e --- /dev/null +++ b/src/Form/Type/Product/ServiceCategorySelectFormType.php @@ -0,0 +1,15 @@ +add('duration', TextType::class, [ + 'label' => 'new_service.form.serviceDuration', + 'label_attr' => ['class' => 'text-black fs-6 fw-normal'], + 'required' => false, + ]); + } + + public function getType(): ProductType + { + return ProductType::SERVICE; + } + + public function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + $resolver->setDefaults([ + 'data_class' => Product::class, + 'attr' => [ + 'novalidate' => 'novalidate', + ], + ]); + } +} diff --git a/src/Form/Type/Security/AccountCreateStep1FormType.php b/src/Form/Type/Security/AccountCreateStep1FormType.php new file mode 100755 index 0000000..dc7b2a3 --- /dev/null +++ b/src/Form/Type/Security/AccountCreateStep1FormType.php @@ -0,0 +1,55 @@ +add('email', EmailType::class, [ + 'label' => 'Adresse e-mail', + 'label_attr' => ['class' => 'fs-6 text-black'], + 'attr' => [ + 'class' => 'form-control-sm', + 'placeholder' => 'monemail@domain.com', + ], + 'empty_data' => '', // allow to have a non nullable type for the email + ]) + ->add('submit', SubmitType::class, [ + 'label' => 'account_create_action.title', + 'attr' => ['class' => 'btn btn-primary btn-sm'], + ]) + ; + + // normalize the email for the Unique constraint to work properly + $this->userManager->addEmailNormalizeSubmitEvent($builder); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + 'translation_domain' => 'security', + 'validation_groups' => $this::class, + ]); + } +} diff --git a/src/Form/Type/Security/AccountCreateStep2FormType.php b/src/Form/Type/Security/AccountCreateStep2FormType.php new file mode 100755 index 0000000..94e677b --- /dev/null +++ b/src/Form/Type/Security/AccountCreateStep2FormType.php @@ -0,0 +1,140 @@ +add('type', ChoiceType::class, [ + 'label' => 'account_create_action.account_type', + 'label_attr' => ['class' => 'text-black fw-light'], + 'choices' => UserType::getForFront(), + 'choice_attr' => function () { + return [ + 'data-controller' => 'account', + 'data-action' => 'click->account#choosenType', + ]; + }, + 'expanded' => true, + ]) + + ->add('firstname', TextType::class, [ + 'label' => 'account_create_action.firsname', + 'label_attr' => ['class' => 'text-black fw-light required'], + 'attr' => [ + 'class' => 'form-control-sm input-firstname', + 'placeholder' => 'account_create_action.firsname.placeholder', + ], + 'required' => false, + ]) + + ->add('lastname', TextType::class, [ + 'label' => 'account_create_action.lastname', + 'label_attr' => ['class' => 'text-black fw-light required'], + 'attr' => [ + 'class' => 'form-control-sm input-lastname', + 'placeholder' => 'account_create_action.lastname.placeholder', + ], + 'required' => false, + ]) + + ->add('name', TextType::class, [ + 'label' => 'account_create_action.name', + 'label_attr' => ['class' => 'text-black fw-light required'], + 'attr' => [ + 'class' => 'form-control-sm input-name', + 'placeholder' => 'account_create_action.name.placeholder', + ], + 'required' => false, + ]) + + ->add('plainPassword', RepeatedType::class, [ + 'type' => PasswordType::class, + 'invalid_message' => 'account_create_action.password.invalid_message', + 'options' => ['attr' => ['class' => 'password-field']], + 'required' => true, + 'first_options' => [ + 'label' => 'account_create_action.password.first', + 'help' => 'account_create_action.password.help', + 'label_attr' => ['class' => 'text-black fw-light'], + 'attr' => [ + 'class' => 'form-control-sm', + 'placeholder' => '', + 'data-password-visibility-target' => 'input', + 'spellcheck' => false, + ], + 'required' => true, + ], + 'second_options' => [ + 'label' => 'account_create_action.password.second', + 'help' => 'account_create_action.password.help', + 'label_attr' => ['class' => 'text-black fw-light'], + 'attr' => [ + 'class' => 'form-control-sm', + 'placeholder' => '', + 'data-password-visibility-target' => 'input', + 'spellcheck' => false, + ], + 'required' => true, + ], + ]) + + ->add('gdpr', CheckboxType::class, [ + 'label' => 'account_create_action.gdpr', + 'label_translation_parameters' => [ + '%link%' => '/fr/cgu', + ], + 'label_html' => true, + 'label_attr' => ['class' => 'fw-light text-black gdpr'], + 'required' => true, + 'data' => false, + 'validation_groups' => [self::class], + ]) + + ->add('submit', SubmitType::class, [ + 'label' => 'account_create_action.title', + 'attr' => ['class' => 'btn btn-primary btn-sm'], + ]) + ; + + $builder->get('type')->addModelTransformer(new CallbackTransformer( + function (?UserType $enumToString) { + return $enumToString === null ? '' : $enumToString->value; + }, + function (string $stringToEnum) { + return UserType::from($stringToEnum); + } + )); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + 'translation_domain' => 'security', + 'validation_groups' => ['Default', self::class], + ]); + } +} diff --git a/src/Form/Type/Security/GroupInvitationFormType.php b/src/Form/Type/Security/GroupInvitationFormType.php new file mode 100755 index 0000000..5804f2d --- /dev/null +++ b/src/Form/Type/Security/GroupInvitationFormType.php @@ -0,0 +1,42 @@ +add('email', EmailType::class, [ + 'label' => $this->getI18nPrefix().'.email', + ]) + ->add('submit', SubmitType::class, [ + 'label' => $this->getI18nPrefix().'.submit', + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + 'translation_domain' => 'admin', + 'validation_groups' => self::class, + ]); + } +} diff --git a/src/Form/Type/Security/LostPasswordFormType.php b/src/Form/Type/Security/LostPasswordFormType.php new file mode 100644 index 0000000..a02c4b5 --- /dev/null +++ b/src/Form/Type/Security/LostPasswordFormType.php @@ -0,0 +1,42 @@ +add('email', EmailType::class, [ + 'label' => 'Adresse e-mail', + 'label_attr' => ['class' => 'text-black fw-light'], + 'attr' => [ + 'class' => 'form-control-sm', + 'placeholder' => 'lost_password.form.email.placeholder', + ], + ]) + + ->add('submit', SubmitType::class, [ + 'label' => 'lost_password.form.submit', + 'attr' => ['class' => 'btn btn-primary btn-sm'], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => LostPasswordCommand::class, + 'translation_domain' => 'security', + ]); + } +} diff --git a/src/Form/Type/Security/ResetPasswordFormType.php b/src/Form/Type/Security/ResetPasswordFormType.php new file mode 100644 index 0000000..cc382d8 --- /dev/null +++ b/src/Form/Type/Security/ResetPasswordFormType.php @@ -0,0 +1,60 @@ +add('password', RepeatedType::class, [ + 'type' => PasswordType::class, + 'invalid_message' => 'account_create_action.password.invalid_message', + 'options' => ['attr' => ['class' => 'password-field']], + 'required' => true, + 'first_options' => [ + 'label' => 'account_create_action.password.first', + 'help' => 'account_create_action.password.help', + 'label_attr' => ['class' => 'text-black fw-light'], + 'attr' => [ + 'class' => 'form-control-sm', + 'data-password-visibility-target' => 'input', + ], + 'required' => true, + ], + 'second_options' => [ + 'label' => 'account_create_action.password.second', + 'help' => 'account_create_action.password.help', + 'label_attr' => ['class' => 'text-black fw-light'], + 'attr' => [ + 'class' => 'form-control-sm', + 'data-password-visibility-target' => 'input', + ], + 'required' => true, + ], + ]) + ->add('submit', SubmitType::class, [ + 'label' => 'Réinitialiser mon mot de passe', + 'attr' => ['class' => 'btn btn-primary btn-sm'], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => ResetPasswordCommand::class, + 'translation_domain' => 'security', + ]); + } +} diff --git a/src/Form/Type/ServiceRequest/AbstractUserProductSelectFormType.php b/src/Form/Type/ServiceRequest/AbstractUserProductSelectFormType.php new file mode 100644 index 0000000..aa6651a --- /dev/null +++ b/src/Form/Type/ServiceRequest/AbstractUserProductSelectFormType.php @@ -0,0 +1,54 @@ +setMethod('GET') + ->add('product', EntityType::class, [ + 'class' => Product::class, + 'query_builder' => function (EntityRepository $entityRepository) { + $qb = $entityRepository->createQueryBuilder('p') + ->from(ServiceRequest::class, 'sr') + ->andWhere('p = sr.product'); + + return $qb->andWhere(sprintf('sr.%s = :user', $this->isOwner() ? 'owner' : 'recipient')) + ->setParameter('user', $this->security->getUser()); + }, + 'required' => false, + 'label' => false, + 'multiple' => true, + 'autocomplete' => true, + ]) + ->add('submit', SubmitType::class); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'csrf_protection' => false, + ]); + } +} diff --git a/src/Form/Type/ServiceRequest/CreateServiceRequestType.php b/src/Form/Type/ServiceRequest/CreateServiceRequestType.php new file mode 100755 index 0000000..966d3ba --- /dev/null +++ b/src/Form/Type/ServiceRequest/CreateServiceRequestType.php @@ -0,0 +1,68 @@ +add('message', TextareaType::class, [ + 'label' => 'loan.new_action.form.message', + 'label_attr' => ['class' => ''], + 'attr' => [ + 'class' => 'form-control', + 'style' => 'height: 120px', + 'placeholder' => '', + ], + 'required' => false, + ]) + ->add('startAt', DateType::class, [ + 'label' => 'loan.new_action.form.startAt', + 'label_attr' => ['class' => ''], + 'attr' => [ + 'class' => '', + 'placeholder' => '', + ], + 'input' => 'datetime_immutable', + 'widget' => 'single_text', + 'html5' => true, // turn this to false to apply a custom date picker + ]) + ->add('endAt', DateType::class, [ + 'label' => 'loan.new_action.form.endAt', + 'label_attr' => ['class' => ''], + 'attr' => [ + 'class' => '', + 'placeholder' => '', + ], + 'input' => 'datetime_immutable', + 'widget' => 'single_text', + 'html5' => true, // turn this to false to apply a custom date picker + ]) + ->add('submit', SubmitType::class, [ + 'label' => 'loan.new_action.form.submit', + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => ServiceRequest::class, + 'validation_groups' => self::class, + ]); + } +} diff --git a/src/Form/Type/ServiceRequest/ModifyServiceRequestType.php b/src/Form/Type/ServiceRequest/ModifyServiceRequestType.php new file mode 100755 index 0000000..01fda94 --- /dev/null +++ b/src/Form/Type/ServiceRequest/ModifyServiceRequestType.php @@ -0,0 +1,45 @@ +add('startAt', DateType::class, [ + 'label' => 'loan.new_action.form.startAt', + 'label_attr' => ['class' => ''], + 'input' => 'datetime_immutable', + 'widget' => 'single_text', + 'html5' => true, + ]) + ->add('endAt', DateType::class, [ + 'label' => 'loan.new_action.form.endAt', + 'label_attr' => ['class' => ''], + 'input' => 'datetime_immutable', + 'widget' => 'single_text', + 'html5' => true, + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => ServiceRequest::class, + 'validation_groups' => CreateServiceRequestType::class, + ]); + } +} diff --git a/src/Form/Type/ServiceRequest/UserLendingProductSelectFormType.php b/src/Form/Type/ServiceRequest/UserLendingProductSelectFormType.php new file mode 100644 index 0000000..fe6ebe9 --- /dev/null +++ b/src/Form/Type/ServiceRequest/UserLendingProductSelectFormType.php @@ -0,0 +1,13 @@ +add('address', TextType::class, [ + 'label' => 'address.step1_action.form.address', + 'label_attr' => ['class' => ''], + 'attr' => [ + 'class' => 'form-control-sm', + 'placeholder' => '', + ], + ]) + + ->add('addressSupplement', TextType::class, [ + 'label' => 'address.step1_action.form.address_supplement', + 'label_attr' => ['class' => ''], + 'attr' => [ + 'class' => 'form-control-sm', + 'placeholder' => '', + ], + 'required' => false, + ]) + + ->add('postalCode', TextType::class, [ + 'label' => 'address.step1_action.form.postal_code', + 'label_attr' => ['class' => ''], + 'attr' => [ + 'class' => 'form-control-sm', + 'placeholder' => '', + ], + ]) + + ->add('locality', TextType::class, [ + 'label' => 'address.step1_action.form.locality', + 'label_attr' => ['class' => ''], + 'attr' => [ + 'class' => 'form-control-sm', + 'placeholder' => '', + ], + ]) + + ->add('country', CountryType::class, [ + 'label' => 'address.step1_action.form.country', + 'preferred_choices' => [GeoProviderInterface::DEFAULT_COUNTRY], + 'attr' => ['class' => 'form-control-sm'], + ]) + + ->add('submit', SubmitType::class, [ + 'label' => 'address.step1_action.form.submit', + 'attr' => ['class' => 'btn-sm btn-primary'], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Address::class, + ]); + } +} diff --git a/src/Form/Type/User/AddressStep2FormType.php b/src/Form/Type/User/AddressStep2FormType.php new file mode 100755 index 0000000..38e9c2d --- /dev/null +++ b/src/Form/Type/User/AddressStep2FormType.php @@ -0,0 +1,55 @@ +getI18nPrefix(); + /** @var array $addresses */ + $addresses = $options['addresses']; + + $builder + ->add('addresses', ChoiceType::class, [ + 'label' => false, + 'label_attr' => [], + 'choices' => $addresses, + 'expanded' => true, + 'choice_label' => 'displayName', + 'constraints' => [ + new NotNull(message: $i18nPrefix.'.addresses.not_null'), + new Choice(choices: $addresses), + ], + ]) + ->add('submit', SubmitType::class, [ + 'label' => $i18nPrefix.'.submit.label', + 'attr' => ['class' => 'btn-sm btn-primary'], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'addresses' => [], + ]); + } +} diff --git a/src/Form/Type/User/ChangeLoginFormType.php b/src/Form/Type/User/ChangeLoginFormType.php new file mode 100644 index 0000000..7eda9eb --- /dev/null +++ b/src/Form/Type/User/ChangeLoginFormType.php @@ -0,0 +1,58 @@ +add('email', RepeatedType::class, [ + 'type' => EmailType::class, + 'required' => true, + 'first_options' => [ + 'label' => self::I18N_PREFIX.'.form.email', + 'label_attr' => ['class' => 'fs-6 text-black'], + 'attr' => [ + 'class' => 'form-control form-control-sm', + 'placeholder' => self::I18N_PREFIX.'.form.email_placeholder', + ], + 'required' => true, + ], + 'second_options' => [ + 'label' => self::I18N_PREFIX.'.form.email_repeat', + 'label_attr' => ['class' => 'fs-6 text-black'], + 'attr' => [ + 'class' => 'form-control form-control-sm', + 'placeholder' => self::I18N_PREFIX.'.form.email_placeholder', + ], + 'required' => true, + ], + ]) + ->add('submit', SubmitType::class, [ + 'label' => self::I18N_PREFIX.'.form.submit', + 'attr' => ['class' => 'btn btn-primary btn-sm'], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + 'validation_groups' => self::class, + ]); + } +} diff --git a/src/Form/Type/User/ChangePasswordFormType.php b/src/Form/Type/User/ChangePasswordFormType.php new file mode 100644 index 0000000..49b3fff --- /dev/null +++ b/src/Form/Type/User/ChangePasswordFormType.php @@ -0,0 +1,68 @@ +add('oldPassword', PasswordType::class, [ + 'label' => self::I18N_PREFIX.'.old', + 'label_attr' => ['class' => 'fs-6 text-black'], + 'attr' => [ + 'class' => 'form-control form-control-sm', + ], + 'required' => true, + ]) + ->add('plainPassword', RepeatedType::class, [ + 'type' => PasswordType::class, + 'required' => true, + 'first_options' => [ + 'label' => self::I18N_PREFIX.'.new', + 'help' => self::I18N_PREFIX.'.help', + 'label_attr' => ['class' => 'fs-6 text-black'], + 'attr' => [ + 'class' => 'form-control form-control-sm', + 'data-password-visibility-target' => 'input', + ], + 'required' => true, + ], + 'second_options' => [ + 'label' => self::I18N_PREFIX.'.confirm', + 'help' => self::I18N_PREFIX.'.help', + 'label_attr' => ['class' => 'fs-6 text-black'], + 'attr' => [ + 'class' => 'form-control form-control-sm', + 'data-password-visibility-target' => 'input', + ], + 'required' => true, + ], + ]) + ->add('submit', SubmitType::class, [ + 'label' => self::I18N_PREFIX.'.submit', + 'attr' => ['class' => 'btn btn-primary btn-sm'], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + 'validation_groups' => self::class, + ]); + } +} diff --git a/src/Form/Type/User/EditProfileFormType.php b/src/Form/Type/User/EditProfileFormType.php new file mode 100644 index 0000000..28e8562 --- /dev/null +++ b/src/Form/Type/User/EditProfileFormType.php @@ -0,0 +1,142 @@ +security->getUser(); + + $builder + ->add('avatar', FileType::class, [ + 'label' => self::I18N_PREFIX.'.image', + 'mapped' => false, + 'required' => false, + 'help' => 'product.form.upload_infos', + ]) + ->add('phone', PhoneNumberType::class, [ + 'label' => self::I18N_PREFIX.'.phone', + 'label_attr' => ['class' => 'text-black fs-6 fw-normal'], + 'format' => PhoneNumberFormat::INTERNATIONAL, + 'widget' => PhoneNumberType::WIDGET_COUNTRY_CHOICE, + 'preferred_country_choices' => ['FR'], + 'required' => false, + ]) + + ->add('smsNotifications', CheckboxType::class, [ + 'label' => self::I18N_PREFIX.'.sms', + 'label_attr' => ['class' => 'text-black fs-6 fw-normal'], + ]) + ->add('submit', SubmitType::class, [ + 'label' => self::I18N_PREFIX.'.submit', + 'attr' => ['class' => 'btn-sm btn-primary'], + ]); + + if ($user->isPlace()) { + $builder + ->add('name', TextType::class, [ + 'label' => self::I18N_PREFIX.'.name', + 'label_attr' => ['class' => 'text-black fs-6 fw-normal'], + 'attr' => [ + 'class' => 'form-control-sm', + ], + ]) + ->add('schedule', TextareaType::class, [ + 'label' => self::I18N_PREFIX.'.schedule', + 'label_attr' => ['class' => 'text-black fs-6 fw-normal'], + 'attr' => ['style' => 'height: 120px'], + 'required' => false, + ]); + } else { + $builder + ->add('firstname', TextType::class, [ + 'label' => self::I18N_PREFIX.'.firstname', + 'label_attr' => ['class' => 'text-black fs-6 fw-normal'], + 'attr' => [ + 'class' => 'form-control-sm', + ], + ]) + ->add('lastname', TextType::class, [ + 'label' => self::I18N_PREFIX.'.lastname', + 'label_attr' => ['class' => 'text-black fs-6 fw-normal'], + 'attr' => [ + 'class' => 'form-control-sm', + ], + ]) + ->add('category', EntityType::class, [ + 'class' => Category::class, + 'query_builder' => fn (CategoryRepository $er) => $er->getHierarchy(), + 'group_by' => fn (Category $category) => $this->translator->trans(self::I18N_PREFIX.'.'.$category->getType()->value), + 'label' => self::I18N_PREFIX.'.category', + 'label_attr' => ['class' => 'text-black fs-6 fw-normal'], + 'choice_label' => 'getNameWithIndent', + 'expanded' => false, + 'required' => false, + ]) + ->add('description', TextareaType::class, [ + 'label' => self::I18N_PREFIX.'.description', + 'label_attr' => ['class' => 'text-black fs-6 fw-normal'], + 'attr' => ['style' => 'height: 120px'], + 'required' => false, + ]); + } + + $builder + ->addEventListener( + FormEvents::POST_SUBMIT, + [$this, 'onPostSubmit'] + ); + } + + /** + * Transforms the phone object to a string if validation is OK. + * + * @see AbstractUserCrudController::updateEntity + */ + public function onPostSubmit(FormEvent $event): void + { + /** @var User $user */ + $user = $event->getData(); + $user->changePhoneNumber($user->phone); + $event->setData($user); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + 'validation_groups' => self::class, + ]); + } +} diff --git a/src/Form/Type/User/ServiceRequest/NewMessageType.php b/src/Form/Type/User/ServiceRequest/NewMessageType.php new file mode 100755 index 0000000..90d96cb --- /dev/null +++ b/src/Form/Type/User/ServiceRequest/NewMessageType.php @@ -0,0 +1,43 @@ +add('message', TextareaType::class, [ + 'label' => 'new_message.form.message', + 'label_attr' => ['class' => ''], + 'attr' => [ + 'class' => '', + 'placeholder' => 'new_message.form.message.placeholder', + ], + 'required' => false, + ]) + ->add('submit', SubmitType::class, [ + 'label' => 'new_message.form.submit', + 'label_html' => true, + 'attr' => ['class' => 'btn btn-primary'], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Message::class, + 'validation_groups' => self::class, + ]); + } +} diff --git a/src/Geocoder/Adapter/NominatimToAddressAdapter.php b/src/Geocoder/Adapter/NominatimToAddressAdapter.php new file mode 100644 index 0000000..ef3085d --- /dev/null +++ b/src/Geocoder/Adapter/NominatimToAddressAdapter.php @@ -0,0 +1,37 @@ +setAddress($nominatimAddress->getStreetNumber().' '.$nominatimAddress->getStreetName()); + if ($nominatimAddress->getCoordinates() !== null) { + $address->setLatitude((string) $nominatimAddress->getCoordinates()->getLatitude()); + $address->setLongitude((string) $nominatimAddress->getCoordinates()->getLongitude()); + } + + $address->setStreetName((string) $nominatimAddress->getStreetName()); + $address->setStreetNumber((string) $nominatimAddress->getStreetNumber()); + $address->setSubLocality((string) $nominatimAddress->getSubLocality()); + $address->setLocality((string) $nominatimAddress->getLocality()); + $address->setPostalCode((string) $nominatimAddress->getPostalCode()); + $address->setProvidedBy($nominatimAddress->getProvidedBy()); + $address->setAttribution((string) $nominatimAddress->getAttribution()); + $address->setDisplayName((string) $nominatimAddress->getDisplayName()); + $address->setOsmType((string) $nominatimAddress->getOSMType()); + + if ($nominatimAddress->getOSMId() !== null) { + $address->setOsmId($nominatimAddress->getOSMId()); + } + } +} diff --git a/src/Geocoder/GeoProvider.php b/src/Geocoder/GeoProvider.php new file mode 100644 index 0000000..f112516 --- /dev/null +++ b/src/Geocoder/GeoProvider.php @@ -0,0 +1,61 @@ +withLimit($limit) + ->withLocale($country) + ; + try { + /** @var AddressCollection $collection */ + $collection = $this->nominatimGeocoder->geocodeQuery($query); + } catch (Exception $e) { + throw new \RuntimeException(sprintf('Unable to get geoloc of %s: %s', $text, $e->getMessage())); + } + + return $collection; + } + + /** + * Get the first result of a query as an Address entity instance. + */ + public function getAddress(string $text): ?Address + { + $collection = $this->getAddressCollection($text, 1); + + // invalid address + if ($collection->isEmpty()) { + return null; + } + + $address = new Address(); + /** @var NominatimAddress $geoAddress */ + $geoAddress = $collection->first(); + $this->adapter->fill($address, $geoAddress); + + return $address; + } +} diff --git a/src/Geocoder/GeoProviderInterface.php b/src/Geocoder/GeoProviderInterface.php new file mode 100644 index 0000000..469ed19 --- /dev/null +++ b/src/Geocoder/GeoProviderInterface.php @@ -0,0 +1,17 @@ +getQuery()->getArrayResult(); + $fieldNames = array_values(array_map(static fn (FieldDto $dto) => $dto->getProperty(), iterator_to_array($fields->getIterator()))); + + $data = []; + foreach ($result as $index => $row) { + /** @var array $row */ + foreach ($row as $columnKey => $columnValue) { + // only allow fields on list + + if (!\in_array($columnKey, $fieldNames, true)) { + continue; + } + + if ($columnValue instanceof \DateTimeInterface) { + $columnValue = $columnValue->format('Y-m-d H:i:s'); // @todo use a parameter + } + + if ($columnValue instanceof \UnitEnum) { + $columnValue = $this->translator->trans($columnValue->name, [], DashboardController::DOMAIN); + } + + /* + if ($columnValue instanceof AbstractUid) { + $columnValue = (string) $columnValue; + } + + if (\is_array($columnValue)) { + $columnValue = implode(' ,', $columnValue); + } + */ + + if (\is_bool($columnValue)) { + $columnValue = $this->translator->trans($columnValue ? 'yes' : 'no', [], DashboardController::DOMAIN); + } + + $data[$index][$columnKey] = $columnValue; + } + } + + // Preserve the fields' order + $orderedFieldNames = array_keys($data[0] ?? []); + + // Humanize headers based on translations (same as EA would do) + $headers = []; + foreach ($orderedFieldNames as $property) { + $headers[$property] = $this->translator->trans($this->stringHelper->humanize($property), [], DashboardController::DOMAIN); + } + array_unshift($data, $headers); + + $response = new StreamedResponse(function () use ($data) { + $config = new ExporterConfig(); + $exporter = new Exporter($config); + $exporter->export('php://output', $data); + }); + $dispositionHeader = $response->headers->makeDisposition( + HeaderUtils::DISPOSITION_ATTACHMENT, + $filename + ); + $response->headers->set('Content-Disposition', $dispositionHeader); + $response->headers->set('Content-Type', 'text/csv; charset=utf-8'); + + return $response; + } +} diff --git a/src/Helper/FileUploader.php b/src/Helper/FileUploader.php new file mode 100644 index 0000000..67f0cab --- /dev/null +++ b/src/Helper/FileUploader.php @@ -0,0 +1,40 @@ + $images + * + * @return array + */ + public function uploadImageArray(FilesystemOperator $storage, array $images): array + { + $imagesUploaded = []; + foreach ($images as $image) { + $imagesUploaded[] = $this->uploadImage($storage, $image); + } + + return $imagesUploaded; + } + + /** + * To upload a single image. + */ + public function uploadImage(FilesystemOperator $storage, UploadedFile $image): string + { + $newFilename = Uuid::v4().'.'.$image->guessExtension(); + $storage->write($newFilename, $image->getContent()); + + return $newFilename; + } +} diff --git a/src/Helper/StringHelper.php b/src/Helper/StringHelper.php new file mode 100644 index 0000000..625cf89 --- /dev/null +++ b/src/Helper/StringHelper.php @@ -0,0 +1,41 @@ +upper()->toString(); + + // this prevents humanizing all-uppercase labels (e.g. 'UUID' -> 'U u i d') + // and other special labels which look better in uppercase + if ($uString->toString() === $upperString || \in_array($upperString, ['ID', 'URL'], true)) { + return $upperString; + } + + return $uString + ->replaceMatches('/([A-Z])/', '_$1') + ->replaceMatches('/[_\s]+/', ' ') + ->trim() + ->lower() + ->title(true) + ->toString(); + } + + /** + * Make the stored email consistent. + */ + public function normalizeEmail(string $email): string + { + return u($email)->trim()->lower()->toString(); + } +} diff --git a/src/Helper/VarDumperHelper.php b/src/Helper/VarDumperHelper.php new file mode 100644 index 0000000..09160a1 --- /dev/null +++ b/src/Helper/VarDumperHelper.php @@ -0,0 +1,26 @@ +dump($cloner->cloneVar($var)); + }); + } +} diff --git a/src/Kernel.php b/src/Kernel.php new file mode 100644 index 0000000..9429265 --- /dev/null +++ b/src/Kernel.php @@ -0,0 +1,48 @@ +getConfigDir(); + + // packages + $container->import($configDir.'/{packages}/*.{php,yaml}'); + $container->import($configDir.'/{packages}/'.$this->environment.'/*.{php,yaml}'); + + // services + $container->import($configDir.'/services.yaml'); + $container->import($configDir.'/{services}_'.$this->environment.'.yaml'); + + // custom extra configuration for packages + $container->import($configDir.'/{packages_extra}/*.{php,yaml}'); + } + + /** + * Additional container stuff. + */ + protected function build(ContainerBuilder $container): void + { + $container->addCompilerPass(new LocalesCompilerPass(), PassConfig::TYPE_OPTIMIZE); + } +} diff --git a/src/Mailer/AppMailer.php b/src/Mailer/AppMailer.php new file mode 100644 index 0000000..3f30a64 --- /dev/null +++ b/src/Mailer/AppMailer.php @@ -0,0 +1,58 @@ + $context + * + * @throws TransportExceptionInterface + */ + public function send(string $emailCode, array $context): void + { + $email = null; + foreach ($this->emailCollection->getEmails() as $appEmail) { + /** @var EmailInterface $appEmail */ + if ($appEmail->supports($emailCode)) { + $email = $appEmail->getEmail($context); + break; + } + } + + if ($email === null) { + throw new \LogicException("No email found to process the $emailCode email"); + } + + $configuration = $this->configurationRepository->getInstanceConfiguration(); + Assert::isInstanceOf($configuration, Configuration::class); + $from = new Address($configuration->getNotificationsSenderEmail(), $configuration->getNotificationsSenderName()); + $email->from($from); + + $this->mailer->send($email); + } +} diff --git a/src/Mailer/Email/Admin/Group/GroupInvitationEmail.php b/src/Mailer/Email/Admin/Group/GroupInvitationEmail.php new file mode 100644 index 0000000..37b6f49 --- /dev/null +++ b/src/Mailer/Email/Admin/Group/GroupInvitationEmail.php @@ -0,0 +1,56 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var User $user */ + $user = $context['user']; + /** @var Group $group */ + $group = $context['group']; + + return (new TemplatedEmail()) + ->to($user->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans($this->getI18nPrefix().'.subject', [ + '%brand%' => $this->brand, + '%group%' => $group->getName(), + ], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/admin/group/group_invitation.html.twig') + ->context($context) + ; + } +} diff --git a/src/Mailer/Email/Admin/New/NewAdminEmail.php b/src/Mailer/Email/Admin/New/NewAdminEmail.php new file mode 100644 index 0000000..4b01f69 --- /dev/null +++ b/src/Mailer/Email/Admin/New/NewAdminEmail.php @@ -0,0 +1,45 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var User $user */ + $user = $context['user']; + + return (new TemplatedEmail()) + ->to($user->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans($this->getI18nPrefix().'.subject', ['%brand%' => $this->brand], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/admin/new/new_admin.html.twig') + ->context($context) + ; + } +} diff --git a/src/Mailer/Email/Admin/PromoteToAdmin/PromoteToAdminEmail.php b/src/Mailer/Email/Admin/PromoteToAdmin/PromoteToAdminEmail.php new file mode 100644 index 0000000..e90c228 --- /dev/null +++ b/src/Mailer/Email/Admin/PromoteToAdmin/PromoteToAdminEmail.php @@ -0,0 +1,45 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var User $user */ + $user = $context['user']; + + return (new TemplatedEmail()) + ->to($user->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans($this->getI18nPrefix().'.subject', ['%brand%' => $this->brand], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/admin/promote_to_admin/promote_to_admin.html.twig') + ->context($context) + ; + } +} diff --git a/src/Mailer/Email/Admin/UserGroup/AdminPromotionEmail.php b/src/Mailer/Email/Admin/UserGroup/AdminPromotionEmail.php new file mode 100644 index 0000000..a2b7bb0 --- /dev/null +++ b/src/Mailer/Email/Admin/UserGroup/AdminPromotionEmail.php @@ -0,0 +1,48 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var User $user */ + $user = $context['user']; + + return (new TemplatedEmail()) + ->to($user->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans($this->getI18nPrefix().'.subject', ['%brand%' => $this->brand], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/admin/user_group/admin_promotion.html.twig') + ->context($context) + ; + } +} diff --git a/src/Mailer/Email/Admin/UserGroup/MainAdminPromotionEmail.php b/src/Mailer/Email/Admin/UserGroup/MainAdminPromotionEmail.php new file mode 100644 index 0000000..861a901 --- /dev/null +++ b/src/Mailer/Email/Admin/UserGroup/MainAdminPromotionEmail.php @@ -0,0 +1,48 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var User $user */ + $user = $context['user']; + + return (new TemplatedEmail()) + ->to($user->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans($this->getI18nPrefix().'.subject', ['%brand%' => $this->brand], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/admin/user_group/main_admin_promotion.html.twig') + ->context($context) + ; + } +} diff --git a/src/Mailer/Email/Command/EndMembershipEmail.php b/src/Mailer/Email/Command/EndMembershipEmail.php new file mode 100644 index 0000000..6ad663d --- /dev/null +++ b/src/Mailer/Email/Command/EndMembershipEmail.php @@ -0,0 +1,59 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var ?User $user */ + $user = $context['user'] ?? null; + Assert::isInstanceOf($user, User::class); + + /** @var ?Group $group */ + $group = $context['group'] ?? null; + Assert::isInstanceOf($group, Group::class); + + return (new TemplatedEmail()) + ->to($user->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans($this->getI18nPrefix().'.subject', [ + '%brand%' => $this->brand, + '%group%' => $group->getName(), + ], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/command/end_membership.html.twig') + ->context($context) + ; + } +} diff --git a/src/Mailer/Email/Command/NotifyMembershipExpirationEmail.php b/src/Mailer/Email/Command/NotifyMembershipExpirationEmail.php new file mode 100644 index 0000000..b6360dc --- /dev/null +++ b/src/Mailer/Email/Command/NotifyMembershipExpirationEmail.php @@ -0,0 +1,59 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var ?User $user */ + $user = $context['user'] ?? null; + Assert::isInstanceOf($user, User::class); + + /** @var ?Group $group */ + $group = $context['group'] ?? null; + Assert::isInstanceOf($group, Group::class); + + return (new TemplatedEmail()) + ->to($user->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans($this->getI18nPrefix().'.subject', [ + '%brand%' => $this->brand, + '%group%' => $group->getName(), + '%days%' => $context['days'], + ], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/command/notify_membership_expiration.html.twig') + ->context($context) + ; + } +} diff --git a/src/Mailer/Email/Command/NotifyServiceRequestEndEmail.php b/src/Mailer/Email/Command/NotifyServiceRequestEndEmail.php new file mode 100644 index 0000000..7570bb5 --- /dev/null +++ b/src/Mailer/Email/Command/NotifyServiceRequestEndEmail.php @@ -0,0 +1,59 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var ?ServiceRequest $sr */ + $sr = $context['service_request'] ?? null; + Assert::isInstanceOf($sr, ServiceRequest::class); + + /** @var ?User $user */ + $user = $context['user'] ?? null; + Assert::isInstanceOf($user, User::class); + + return (new TemplatedEmail()) + ->to($user->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans($this->getI18nPrefix().'.subject', [ + '%brand%' => $this->brand, + '%product%' => $sr->getProduct()->getName(), + '%date%' => $sr->getEndAt()->format($this->translator->trans('format.date', [], 'date')), + ], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/command/notify_service_request_end.html.twig') + ->context($context) + ; + } +} diff --git a/src/Mailer/Email/Command/NotifyServiceRequestStartEmail.php b/src/Mailer/Email/Command/NotifyServiceRequestStartEmail.php new file mode 100644 index 0000000..42b44da --- /dev/null +++ b/src/Mailer/Email/Command/NotifyServiceRequestStartEmail.php @@ -0,0 +1,59 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var ?ServiceRequest $sr */ + $sr = $context['service_request'] ?? null; + Assert::isInstanceOf($sr, ServiceRequest::class); + + /** @var ?User $user */ + $user = $context['user'] ?? null; + Assert::isInstanceOf($user, User::class); + + return (new TemplatedEmail()) + ->to($user->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans($this->getI18nPrefix().'.subject', [ + '%brand%' => $this->brand, + '%product%' => $sr->getProduct()->getName(), + '%date%' => $sr->getStartAt()->format($this->translator->trans('format.date', [], 'date')), + ], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/command/notify_service_request_start.html.twig') + ->context($context) + ; + } +} diff --git a/src/Mailer/Email/EmailInterface.php b/src/Mailer/Email/EmailInterface.php new file mode 100644 index 0000000..c99946b --- /dev/null +++ b/src/Mailer/Email/EmailInterface.php @@ -0,0 +1,24 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail; +} diff --git a/src/Mailer/Email/EmailTrait.php b/src/Mailer/Email/EmailTrait.php new file mode 100644 index 0000000..c31afc7 --- /dev/null +++ b/src/Mailer/Email/EmailTrait.php @@ -0,0 +1,16 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var User $user */ + $user = $context['user']; + Assert::stringNotEmpty($user->getConfirmationToken(), 'Cannot sent the confirmation email for a user without confirmationToken.'); + + // Is it an email for an invitation? Yes, if the context has a group object associated. + /** @var ?Group $group */ + $group = $context['group'] ?? null; + $subjectKey = $this->getI18nPrefix().'.subject'; + if (isset($context['group'])) { + $subjectKey .= '.invitation'; + } + + return (new TemplatedEmail()) + ->to($user->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans($subjectKey, [ + '%brand%' => $this->brand, + '%group%' => $group?->getName(), + ], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/security/create_account_step1.html.twig') + ->context($context) + ; + } +} diff --git a/src/Mailer/Email/Security/LostPasswordEmail.php b/src/Mailer/Email/Security/LostPasswordEmail.php new file mode 100644 index 0000000..ee47ea4 --- /dev/null +++ b/src/Mailer/Email/Security/LostPasswordEmail.php @@ -0,0 +1,45 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var User $user */ + $user = $context['user']; + $token = $user->getLostPasswordToken(); + Assert::stringNotEmpty($token, 'Cannot sent the email for a user without lost password token'); + + return (new TemplatedEmail()) + ->to($user->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans('lost_password.email.subject', [], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/security/lost_password.html.twig') + ->context($context); + } +} diff --git a/src/Mailer/Email/ServiceRequest/NewMessageEmail.php b/src/Mailer/Email/ServiceRequest/NewMessageEmail.php new file mode 100644 index 0000000..c66fc4c --- /dev/null +++ b/src/Mailer/Email/ServiceRequest/NewMessageEmail.php @@ -0,0 +1,53 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var ?ServiceRequest $serviceRequest */ + $serviceRequest = $context['service_request'] ?? null; + Assert::isInstanceOf($serviceRequest, ServiceRequest::class); + + /** @var ?Message $message */ + $message = $context['message'] ?? null; + Assert::isInstanceOf($message, Message::class); + + return (new TemplatedEmail()) + ->to($message->getRecipient()->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans('new_message.subject', ['%brand%' => $this->brand], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/service_request/message/new.html.twig') + ->context($context) + ; + } +} diff --git a/src/Mailer/Email/ServiceRequest/NewServiceRequest.php b/src/Mailer/Email/ServiceRequest/NewServiceRequest.php new file mode 100644 index 0000000..f6f4e02 --- /dev/null +++ b/src/Mailer/Email/ServiceRequest/NewServiceRequest.php @@ -0,0 +1,51 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var ?ServiceRequest $serviceRequest */ + $serviceRequest = $context['service_request'] ?? null; + Assert::isInstanceOf($serviceRequest, ServiceRequest::class); + + return (new TemplatedEmail()) + ->to($serviceRequest->getOwner()->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans($this->getI18nPrefix().'.subject', ['%brand%' => $this->brand], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/service_request/new.html.twig') + ->context($context) + ; + } +} diff --git a/src/Mailer/Email/ServiceRequest/ServiceRequestAccepted.php b/src/Mailer/Email/ServiceRequest/ServiceRequestAccepted.php new file mode 100644 index 0000000..f6d7f47 --- /dev/null +++ b/src/Mailer/Email/ServiceRequest/ServiceRequestAccepted.php @@ -0,0 +1,52 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var ?ServiceRequest $serviceRequest */ + $serviceRequest = $context['service_request'] ?? null; + Assert::isInstanceOf($serviceRequest, ServiceRequest::class); + + return (new TemplatedEmail()) + ->to($serviceRequest->getRecipient()->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans($this->getI18nPrefix().'.subject', ['%brand%' => $this->brand], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/service_request/accepted.html.twig') + ->context($context); + } +} diff --git a/src/Mailer/Email/ServiceRequest/ServiceRequestConfirmed.php b/src/Mailer/Email/ServiceRequest/ServiceRequestConfirmed.php new file mode 100644 index 0000000..614818c --- /dev/null +++ b/src/Mailer/Email/ServiceRequest/ServiceRequestConfirmed.php @@ -0,0 +1,53 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var ?ServiceRequest $serviceRequest */ + $serviceRequest = $context['service_request'] ?? null; + Assert::isInstanceOf($serviceRequest, ServiceRequest::class); + + return (new TemplatedEmail()) + ->to($serviceRequest->getOwner()->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans($this->getI18nPrefix().'.subject', ['%brand%' => $this->brand], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/service_request/confirmed.html.twig') + ->context($context); + } +} diff --git a/src/Mailer/Email/ServiceRequest/ServiceRequestModifiedByOwner.php b/src/Mailer/Email/ServiceRequest/ServiceRequestModifiedByOwner.php new file mode 100644 index 0000000..7c6393e --- /dev/null +++ b/src/Mailer/Email/ServiceRequest/ServiceRequestModifiedByOwner.php @@ -0,0 +1,54 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var ?ServiceRequest $serviceRequest */ + $serviceRequest = $context['service_request'] ?? null; + Assert::isInstanceOf($serviceRequest, ServiceRequest::class); + $context['modified_by'] = $serviceRequest->getOwner()->getDisplayName(); + + return (new TemplatedEmail()) + ->to($serviceRequest->getRecipient()->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans($this->getI18nPrefix().'.subject', ['%brand%' => $this->brand], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/service_request/modified_by.html.twig') + ->context($context); + } +} diff --git a/src/Mailer/Email/ServiceRequest/ServiceRequestModifiedByRecipient.php b/src/Mailer/Email/ServiceRequest/ServiceRequestModifiedByRecipient.php new file mode 100644 index 0000000..f5c4e10 --- /dev/null +++ b/src/Mailer/Email/ServiceRequest/ServiceRequestModifiedByRecipient.php @@ -0,0 +1,54 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var ?ServiceRequest $serviceRequest */ + $serviceRequest = $context['service_request'] ?? null; + Assert::isInstanceOf($serviceRequest, ServiceRequest::class); + $context['modified_by'] = $serviceRequest->getRecipient()->getDisplayName(); + + return (new TemplatedEmail()) + ->to($serviceRequest->getOwner()->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans($this->getI18nPrefix().'.subject', ['%brand%' => $this->brand], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/service_request/modified_by.html.twig') + ->context($context); + } +} diff --git a/src/Mailer/Email/ServiceRequest/ServiceRequestRefused.php b/src/Mailer/Email/ServiceRequest/ServiceRequestRefused.php new file mode 100644 index 0000000..85ee151 --- /dev/null +++ b/src/Mailer/Email/ServiceRequest/ServiceRequestRefused.php @@ -0,0 +1,55 @@ + $context + */ + public function getEmail(array $context): TemplatedEmail + { + /** @var ?ServiceRequest $serviceRequest */ + $serviceRequest = $context['service_request'] ?? null; + Assert::isInstanceOf($serviceRequest, ServiceRequest::class); + /** @var ?User $actor */ + $actor = $context['actor'] ?? null; + + return (new TemplatedEmail()) + ->to($serviceRequest->getOtherUser($actor)->getEmail()) + ->priority(Email::PRIORITY_HIGH) + ->subject($this->translator->trans($this->getI18nPrefix().'.subject', ['%brand%' => $this->brand], AppMailer::TR_DOMAIN)) + ->htmlTemplate('email/service_request/refused.html.twig') + ->context($context); + } +} diff --git a/src/Mailer/EmailCollection.php b/src/Mailer/EmailCollection.php new file mode 100644 index 0000000..3f53194 --- /dev/null +++ b/src/Mailer/EmailCollection.php @@ -0,0 +1,29 @@ + $emails + */ + public function __construct( + private readonly iterable $emails, + ) { + } + + /** + * @return iterable + */ + public function getEmails(): iterable + { + return $this->emails; + } +} diff --git a/src/Message/Command/Admin/AbstractFormCommand.php b/src/Message/Command/Admin/AbstractFormCommand.php new file mode 100644 index 0000000..19089cb --- /dev/null +++ b/src/Message/Command/Admin/AbstractFormCommand.php @@ -0,0 +1,50 @@ + + */ + abstract protected function getSections(): array; + + /** + * Convert the DTO so it can be stored in the database. + * + * @todo Should be tranform ? + * + * @return array> + */ + public function toJsonArray(): array + { + foreach (array_keys(get_class_vars($this::class)) as $classVar) { + $array[$this->getSection($classVar)][$classVar] = $this->{$classVar}; // @phpstan-ignore-line + } + + return $array ?? []; + } + + /** + * Extract the section from a property name. + */ + protected function getSection(string $classVar): string + { + foreach ($this->getSections() as $section) { + if (u($classVar)->startsWith($section)) { + return $section; + } + } + + throw new \UnexpectedValueException(sprintf('Invalid property name, it should start with "%s"', implode(', ', $this->getSections()))); + } +} diff --git a/src/Message/Command/Admin/ParametersFormCommand.php b/src/Message/Command/Admin/ParametersFormCommand.php new file mode 100755 index 0000000..7e836af --- /dev/null +++ b/src/Message/Command/Admin/ParametersFormCommand.php @@ -0,0 +1,80 @@ + + */ + protected function getSections(): array + { + return [ + 'notificationsSender', + 'contact', + 'groups', + 'articles', + 'confidentiality', + ]; + } + + /** + * Hydrate the DTO from the database settings. + * + * @todo Should be reverse tranform ? + */ + public function hydrate(Configuration $configuration): self + { + $instanceConfiguration = $configuration->getConfiguration(); + foreach (array_keys(get_class_vars($this::class)) as $classVar) { + $this->{$classVar} = $instanceConfiguration[$this->getSection($classVar)][$classVar]; // @phpstan-ignore-line + } + + return $this; + } +} diff --git a/src/Message/Command/Group/CreateGroupInvitationMessage.php b/src/Message/Command/Group/CreateGroupInvitationMessage.php new file mode 100755 index 0000000..4683e4a --- /dev/null +++ b/src/Message/Command/Group/CreateGroupInvitationMessage.php @@ -0,0 +1,23 @@ +getEmail(); + Assert::stringNotEmpty($email, 'The email of a user cannot be null or empty'); + Assert::email($email, 'The email is invalid'); + $this->email = $email; + } +} diff --git a/src/Message/Command/Security/AccountCreateStep2Command.php b/src/Message/Command/Security/AccountCreateStep2Command.php new file mode 100755 index 0000000..e3fb6dd --- /dev/null +++ b/src/Message/Command/Security/AccountCreateStep2Command.php @@ -0,0 +1,37 @@ +id = $user->getId(); + Assert::notNull($user->getType()); + $this->type = $user->getType(); + $this->firstname = $user->getFirstname(); + $this->lastname = $user->getLastname(); + $this->name = $user->getName(); + Assert::stringNotEmpty($user->getPlainPassword()); + $this->plainPassword = $user->getPlainPassword(); + } +} diff --git a/src/Message/Command/Security/AccountCreateStep2RefreshCommand.php b/src/Message/Command/Security/AccountCreateStep2RefreshCommand.php new file mode 100755 index 0000000..e26a01c --- /dev/null +++ b/src/Message/Command/Security/AccountCreateStep2RefreshCommand.php @@ -0,0 +1,17 @@ +id = $id; + } +} diff --git a/src/Message/Command/Security/LostPasswordCommand.php b/src/Message/Command/Security/LostPasswordCommand.php new file mode 100644 index 0000000..df7b06e --- /dev/null +++ b/src/Message/Command/Security/LostPasswordCommand.php @@ -0,0 +1,17 @@ +type === self::VACATION; + } + + public function hasType(): bool + { + return !u($this->type)->isEmpty(); + } +} diff --git a/src/Message/Command/User/Product/CreateProductUnavailabilityCommand.php b/src/Message/Command/User/Product/CreateProductUnavailabilityCommand.php new file mode 100644 index 0000000..560674d --- /dev/null +++ b/src/Message/Command/User/Product/CreateProductUnavailabilityCommand.php @@ -0,0 +1,23 @@ +getAddress()); + $this->address = $address->getAddress(); + $this->addressSupplement = $address->getAddressSupplement(); + Assert::stringNotEmpty($address->getLocality()); + $this->locality = $address->getLocality(); + Assert::stringNotEmpty($address->getPostalCode()); + $this->postalCode = $address->getPostalCode(); + Assert::stringNotEmpty($address->getCountry()); + $this->country = $address->getCountry(); + } + + public string $address; + public ?string $addressSupplement = null; + public string $locality; + public string $postalCode; + public string $country; + + /** + * Format a full address with the user input. We don't use the supplument or + * nothing is found by the provider when using it. + */ + public function getAddressForQuery(): string + { + return $this->address.', '.$this->postalCode.', '.$this->locality; + } +} diff --git a/src/Message/Query/Group/GetGroupByIdQuery.php b/src/Message/Query/Group/GetGroupByIdQuery.php new file mode 100644 index 0000000..1450695 --- /dev/null +++ b/src/Message/Query/Group/GetGroupByIdQuery.php @@ -0,0 +1,21 @@ +userId = $user?->getId(); + } +} diff --git a/src/Message/Query/Product/GetProductByIdQuery.php b/src/Message/Query/Product/GetProductByIdQuery.php new file mode 100644 index 0000000..e010e72 --- /dev/null +++ b/src/Message/Query/Product/GetProductByIdQuery.php @@ -0,0 +1,25 @@ +token = $token; + } +} diff --git a/src/Message/Query/Security/ResetPasswordQuery.php b/src/Message/Query/Security/ResetPasswordQuery.php new file mode 100644 index 0000000..990633b --- /dev/null +++ b/src/Message/Query/Security/ResetPasswordQuery.php @@ -0,0 +1,21 @@ +token = $token; + } +} diff --git a/src/Message/Query/User/Account/GetUserQuery.php b/src/Message/Query/User/Account/GetUserQuery.php new file mode 100644 index 0000000..76d7357 --- /dev/null +++ b/src/Message/Query/User/Account/GetUserQuery.php @@ -0,0 +1,20 @@ +|ArrayCollection|null $products */ + public readonly mixed $products, + ) { + } +} diff --git a/src/Message/Query/User/ServiceRequest/GetLoansQuery.php b/src/Message/Query/User/ServiceRequest/GetLoansQuery.php new file mode 100644 index 0000000..7ffb4dc --- /dev/null +++ b/src/Message/Query/User/ServiceRequest/GetLoansQuery.php @@ -0,0 +1,25 @@ +|ArrayCollection|null $products */ + public readonly mixed $products, + ) { + } +} diff --git a/src/Message/Query/User/ServiceRequest/GetServiceRequestByIdQuery.php b/src/Message/Query/User/ServiceRequest/GetServiceRequestByIdQuery.php new file mode 100644 index 0000000..d6dd9c4 --- /dev/null +++ b/src/Message/Query/User/ServiceRequest/GetServiceRequestByIdQuery.php @@ -0,0 +1,19 @@ +messageBus = $commandBus; + } + + public function dispatch(object $query): mixed + { + return $this->handle($query); + } +} diff --git a/src/MessageBus/CommandBusInterface.php b/src/MessageBus/CommandBusInterface.php new file mode 100644 index 0000000..a012fd1 --- /dev/null +++ b/src/MessageBus/CommandBusInterface.php @@ -0,0 +1,10 @@ +messageBus = $queryBus; + } + + public function query(object $query): mixed + { + return $this->handle($query); + } +} diff --git a/src/MessageBus/QueryBusInterface.php b/src/MessageBus/QueryBusInterface.php new file mode 100644 index 0000000..9b9e9bb --- /dev/null +++ b/src/MessageBus/QueryBusInterface.php @@ -0,0 +1,10 @@ +configurationRepository->getInstanceConfigurationOrCreate(); + $configuration->setConfiguration($message->toJsonArray()); + $this->configurationRepository->save($configuration, true); + } +} diff --git a/src/MessageHandler/Command/Payment/DoneCommandHandler.php b/src/MessageHandler/Command/Payment/DoneCommandHandler.php new file mode 100644 index 0000000..a8ce8d4 --- /dev/null +++ b/src/MessageHandler/Command/Payment/DoneCommandHandler.php @@ -0,0 +1,77 @@ +groupOfferRepository->get($message->groupOfferId); + $group = $groupOffer->getGroup(); + $user = $this->userRepository->get($message->userId); + + $gateway = $this->payum->getGateway($message->paymentToken->getGatewayName()); + $status = new GetHumanStatus($message->paymentToken); + $gateway->execute($status); + + // /** @var Payment $payment */ + // $payment = $status->getFirstModel(); + + // Not captured + if (!$status->isCaptured()) { + return $status; + } + + // user has an invitation for this group + if ($user->hasLink($group)) { + /** @var UserGroup $userGroup */ + $userGroup = $user->getGroupMembership($group); + } else { + $userGroup = (new UserGroup()) + ->setUser($user) + ->setGroup($groupOffer->getGroup()); + } + + // promote to member + $userGroup + ->setMembership(UserMembership::MEMBER) + ->setStartAt(CarbonImmutable::today()) + ->setPayedAt(CarbonImmutable::now()) + ; + + // set the end date for recurring offers + $offerType = $groupOffer->getType(); + if ($offerType->isRecurring()) { + $userGroup->setEndAt(new CarbonImmutable($offerType->getEndAtInterval())); + } + $user->addUserGroup($userGroup); + $this->userManager->save($user, true); + + // payment was captured and membership is saved so invalidate the token + $this->payum->getHttpRequestVerifier()->invalidate($message->paymentToken); + + return $status; + } +} diff --git a/src/MessageHandler/Command/Product/CreateGroupInvitationMessageHandler.php b/src/MessageHandler/Command/Product/CreateGroupInvitationMessageHandler.php new file mode 100644 index 0000000..da39fd9 --- /dev/null +++ b/src/MessageHandler/Command/Product/CreateGroupInvitationMessageHandler.php @@ -0,0 +1,83 @@ +groupRepository->get($message->groupId); + Assert::notEmpty($message->email); + Assert::email($message->email); + $email = $this->stringHelper->normalizeEmail($message->email); + $user = $this->userRepository->findOneByEmail($email); + $isNewUser = false; + + // user not found, so we must create a new account (like step1 on the standard workflow) + if ($user === null) { + $user = $this->userManager->getStep1User($message->email); + $this->userManager->save($user, true); + $isNewUser = true; + } + + // now create the invitation to the group. + // check that the user hasn't already have the invitation or doesn't have another role + if (!$user->hasLink($group)) { + $this->userManager->addInvitation($user, $group); + $this->userManager->save($user, true); + } + + // We just ignore if something is already found. It's not a critial error. + // For ewample an admin has sent twice the invitation to the same user because + // he has forgot he has already done it. + + // the notification email is not the same as a new user must confirm its email + $email = $isNewUser ? CreateAccountStep1Email::class : GroupInvitationEmail::class; + $this->appMailer->send($email, compact('user', 'group')); + if (!$isNewUser) { + $this->sendSms($user, GroupInvitationEmail::class, [ + '%group%' => $group->getName(), + ]); + } + } +} diff --git a/src/MessageHandler/Command/Product/CreateProductAvailabilityHandler.php b/src/MessageHandler/Command/Product/CreateProductAvailabilityHandler.php new file mode 100644 index 0000000..e653577 --- /dev/null +++ b/src/MessageHandler/Command/Product/CreateProductAvailabilityHandler.php @@ -0,0 +1,34 @@ +productRepository->find($message->productId); + Assert::isInstanceOf($product, Product::class); + + $newProductAvailability = ProductAvailability::productAvailabilityCreationByOwner($product, $message->startAt, $message->endAt); + $this->productAvailabilityRepository->save($newProductAvailability, true); + + return $newProductAvailability; + } +} diff --git a/src/MessageHandler/Command/Product/DuplicateProductCommandHandler.php b/src/MessageHandler/Command/Product/DuplicateProductCommandHandler.php new file mode 100644 index 0000000..99997ef --- /dev/null +++ b/src/MessageHandler/Command/Product/DuplicateProductCommandHandler.php @@ -0,0 +1,39 @@ +productRepository->get($message->productId); + if ($message->attribute !== null && !$this->authorizationChecker->isGranted($message->attribute, $product)) { + throw new AccessDeniedException(); + } + $duplicated = $this->productManager->duplicate($product); + $this->productManager->save($duplicated, true); + + return $duplicated; + } +} diff --git a/src/MessageHandler/Command/Security/AccountCreateStep1CommandHandler.php b/src/MessageHandler/Command/Security/AccountCreateStep1CommandHandler.php new file mode 100644 index 0000000..6a91d81 --- /dev/null +++ b/src/MessageHandler/Command/Security/AccountCreateStep1CommandHandler.php @@ -0,0 +1,40 @@ +setEmail($message->email); + $this->userManager->refreshConfirmationToken($user); + $this->userManager->save($user, true); + $this->appMailer->send(CreateAccountStep1Email::class, compact('user')); + } +} diff --git a/src/MessageHandler/Command/Security/AccountCreateStep2CommandHandler.php b/src/MessageHandler/Command/Security/AccountCreateStep2CommandHandler.php new file mode 100644 index 0000000..564df19 --- /dev/null +++ b/src/MessageHandler/Command/Security/AccountCreateStep2CommandHandler.php @@ -0,0 +1,54 @@ +userRepository->find($message->id); + Assert::isInstanceOf($user, User::class); + + $user->setType($message->type); + switch ($message->type) { + case UserType::USER: + Assert::stringNotEmpty($message->firstname, 'The firstname is mandatory'); + $user->setFirstname($message->firstname); + Assert::stringNotEmpty($message->lastname, 'The lastname is mandatory'); + $user->setLastname($message->lastname); + $user->setName($message->name); + break; + + case UserType::PLACE: + $user->setFirstname(null); + $user->setLastname(null); + Assert::stringNotEmpty($message->name, 'The name is mandatory'); + $user->setName($message->name); + break; + + default: + throw new \UnexpectedValueException('This hanlder can only create users or places.'); + } + + $this->userManager->updatePassword($user->setPlainPassword($message->plainPassword)); + $this->userManager->finalizeAccountCreateStep2($user); + $this->userManager->save($user, true); + } +} diff --git a/src/MessageHandler/Command/Security/AccountCreateStep2RefreshCommandHandler.php b/src/MessageHandler/Command/Security/AccountCreateStep2RefreshCommandHandler.php new file mode 100644 index 0000000..5fa3984 --- /dev/null +++ b/src/MessageHandler/Command/Security/AccountCreateStep2RefreshCommandHandler.php @@ -0,0 +1,38 @@ +userRepository->find($message->id); + Assert::isInstanceOf($user, User::class); + $this->userManager->refreshConfirmationToken($user); + $this->userManager->save($user, true); + $this->appMailer->send(CreateAccountStep1Email::class, compact('user')); + } +} diff --git a/src/MessageHandler/Command/Security/LostPasswordCommandHandler.php b/src/MessageHandler/Command/Security/LostPasswordCommandHandler.php new file mode 100644 index 0000000..6400108 --- /dev/null +++ b/src/MessageHandler/Command/Security/LostPasswordCommandHandler.php @@ -0,0 +1,47 @@ +email); + $email = $this->stringHelper->normalizeEmail($message->email); + $user = $this->userRepository->findOneByEmail($email); + + // we don't tell the user the email was not found for security + if ($user === null) { + return; + } + + $this->userManager->refreshLostPasswordToken($user); + $this->userManager->save($user, true); + $this->appMailer->send(LostPasswordEmail::class, compact('user')); + } +} diff --git a/src/MessageHandler/Command/Security/ResetPasswordCommandHandler.php b/src/MessageHandler/Command/Security/ResetPasswordCommandHandler.php new file mode 100644 index 0000000..b7cadc4 --- /dev/null +++ b/src/MessageHandler/Command/Security/ResetPasswordCommandHandler.php @@ -0,0 +1,37 @@ +userRepository->find($message->id); + Assert::isInstanceOf($user, User::class); + $this->userManager->updatePassword($user->setPlainPassword($message->password)); + $user->resetLostPawword(); + + // we consider the reset password also act as an email confirmation + // we could also confirm the email as soon the user access the url with the token + $user->confirmEmail(); + $user->resetConfirmation(); + + $this->userManager->save($user, true); + } +} diff --git a/src/MessageHandler/Command/User/ChangeLoginCommandHandler.php b/src/MessageHandler/Command/User/ChangeLoginCommandHandler.php new file mode 100644 index 0000000..b83fe91 --- /dev/null +++ b/src/MessageHandler/Command/User/ChangeLoginCommandHandler.php @@ -0,0 +1,33 @@ +userRepository->find($message->id); + Assert::isInstanceOf($user, User::class); + $this->userManager->changeLogin($user, $message->email); + $this->userManager->save($user, true); + } +} diff --git a/src/MessageHandler/Command/User/ChangeVacationModeCommandHandler.php b/src/MessageHandler/Command/User/ChangeVacationModeCommandHandler.php new file mode 100644 index 0000000..a69980b --- /dev/null +++ b/src/MessageHandler/Command/User/ChangeVacationModeCommandHandler.php @@ -0,0 +1,31 @@ +userRepository->find($message->id); + Assert::isInstanceOf($user, User::class); + $user->switchVacationMode($user->getVacationMode()); + + $this->userManager->save($user, true); + } +} diff --git a/src/MessageHandler/Command/User/Group/AcceptGroupInvitationCommandHandler.php b/src/MessageHandler/Command/User/Group/AcceptGroupInvitationCommandHandler.php new file mode 100644 index 0000000..4fcb3b3 --- /dev/null +++ b/src/MessageHandler/Command/User/Group/AcceptGroupInvitationCommandHandler.php @@ -0,0 +1,37 @@ +groupRepository->get($message->groupId); + $user = $this->userRepository->get($message->userId); + + $membership = $user->getGroupMembership($group); + if ($membership === null) { + throw new UnprocessableEntityHttpException('Membership not found.'); + } + + $membership->setMember(); + $this->userManager->save($user, true); + } +} diff --git a/src/MessageHandler/Command/User/Group/JoinGroupCommandHandler.php b/src/MessageHandler/Command/User/Group/JoinGroupCommandHandler.php new file mode 100644 index 0000000..5d97f98 --- /dev/null +++ b/src/MessageHandler/Command/User/Group/JoinGroupCommandHandler.php @@ -0,0 +1,48 @@ +groupRepository->get($message->groupId); + $user = $this->userRepository->get($message->userId); + + // 1. test if group is public + if (!$group->getType()->isPublic()) { + throw new AccessDeniedHttpException('Group is not public and can only be joined with an invitation link.'); + } + + // 2. test if group is NOT free, user must pay even he is invited + if ($group->hasActiveOffers()) { + throw new AccessDeniedHttpException('Group has paying offers, the user must pay.'); + } + + // 3. test if the user is not already member of the group + if ($user->hasLink($group)) { + return; + } + + // 4. Save in db with the member status + $this->userManager->addToGroup($user, $group); + $this->userManager->save($user, true); + } +} diff --git a/src/MessageHandler/Command/User/Group/QuitGroupCommandHandler.php b/src/MessageHandler/Command/User/Group/QuitGroupCommandHandler.php new file mode 100644 index 0000000..10f797f --- /dev/null +++ b/src/MessageHandler/Command/User/Group/QuitGroupCommandHandler.php @@ -0,0 +1,67 @@ +groupRepository->get($message->groupId); + $user = $this->userRepository->get($message->userId); + $membership = $user->getGroupMembership($group); + if ($membership === null) { + throw new UnprocessableEntityHttpException('Membership not found.'); + } + $user->removeUserGroup($membership); + + // get all products associated to this group + /** @var array $products */ + $products = $this->productRepository->getUserProductsByType($user, null, null, $group)->execute(); + foreach ($products as $product) { + $product->removeGroup($group); + + // this is a security: we must pause the object if the product is not + // associated to other groups, because we don't want the product to + // be public without the user consent. He will have to unpause the product + // to make it searchable again by other users. + if ($product->getGroups()->isEmpty()) { + $product->setPublic(); + $product->setPaused(); + } + + // user choice for products (popup on quit group) + if ($message->hasType()) { + if ($message->isVacation()) { + $product->setPaused(); + } else { + $product->setPublic(); + $product->setActive(); // here we can activate the product as it is the user choice + } + } + + $this->productRepository->save($product, true); + } + + $this->userManager->save($user, true); + } +} diff --git a/src/MessageHandler/Command/User/ServiceRequest/CreateMessageCommandHandler.php b/src/MessageHandler/Command/User/ServiceRequest/CreateMessageCommandHandler.php new file mode 100644 index 0000000..6c67b0d --- /dev/null +++ b/src/MessageHandler/Command/User/ServiceRequest/CreateMessageCommandHandler.php @@ -0,0 +1,50 @@ +serviceRequestRepository->get($message->requestServiceId); + $sender = $this->userRepository->get($message->senderId); + + if ($serviceRequest->isOwner($sender)) { + $message = $this->messageManager->createFromOwnerMessage($serviceRequest, $message->message); + } else { + $message = $this->messageManager->createFromRecipientMessage($serviceRequest, $message->message); + } + $this->messageManager->save($message, true); + + // Send email + $this->appMailer->send(NewMessageEmail::class, ['service_request' => $serviceRequest, 'message' => $message]); + } +} diff --git a/src/MessageHandler/Command/User/ServiceRequest/CreateServiceRequestCommandHandler.php b/src/MessageHandler/Command/User/ServiceRequest/CreateServiceRequestCommandHandler.php new file mode 100644 index 0000000..f8de69a --- /dev/null +++ b/src/MessageHandler/Command/User/ServiceRequest/CreateServiceRequestCommandHandler.php @@ -0,0 +1,94 @@ +productRepository->get($message->productId); + $recipient = $this->userRepository->get($message->recipientId); + $serviceRequest = $product->createServiceRequest($recipient, $message->startAt, $message->endAt); + $this->serviceRequestManager->save($serviceRequest, true); + + // Initialize the conversation + $dateFormat = $this->translator->trans('format.date', [], 'date'); + $systemMessage = $this->messageManager->createSystemMessage($serviceRequest, self::MESSAGE_SYSTEM_NEW, [ + '%recipient%' => $recipient->getDisplayName(), + '%startAt%' => $serviceRequest->getStartAt()->format($dateFormat), + '%endAt%' => $serviceRequest->getEndAt()->format($dateFormat), + ]); + $this->messageManager->save($systemMessage, true); + + // Optional user message + if ($message->message !== null) { + $createdAt = $systemMessage->getCreatedAt()->modify('+1 second'); // add 1 second, so the messages order is more natural + $userMessage = $this->messageManager->createFromRecipientMessage($serviceRequest, $message->message, $createdAt); + $this->messageManager->save($userMessage, true); + } + + // modifiy the product availability + $pa = $this->productAvailabilityManager->createFromServiceRequest($serviceRequest, $message->startAt, $message->endAt); + $this->productAvailabilityManager->save($pa, true); + + // Send email&sms + $this->appMailer->send(NewServiceRequest::class, ['service_request' => $serviceRequest]); + $this->sendSms($serviceRequest); + + return $serviceRequest; + } + + private function sendSms(ServiceRequest $serviceRequest): void + { + $i18nPrefix = $this->getI18nPrefix(NewServiceRequest::class); + $subject = $this->translator->trans($i18nPrefix.'.subject', ['%brand%' => $this->brand], AppMailer::TR_DOMAIN); + $this->notifier->notify( + $serviceRequest->getOwner(), + $subject, + ); + } +} diff --git a/src/MessageHandler/Command/User/ServiceRequest/ReadMessagesCommandHandler.php b/src/MessageHandler/Command/User/ServiceRequest/ReadMessagesCommandHandler.php new file mode 100644 index 0000000..b702251 --- /dev/null +++ b/src/MessageHandler/Command/User/ServiceRequest/ReadMessagesCommandHandler.php @@ -0,0 +1,32 @@ +serviceRequestRepository->get($message->requestServiceId); + $reader = $this->userRepository->get($message->readerId); + $this->serviceRequestManager->readMessages($serviceRequest, $reader); + } +} diff --git a/src/MessageHandler/Command/User/ServiceRequest/TryAutoFinalizeCommandHandler.php b/src/MessageHandler/Command/User/ServiceRequest/TryAutoFinalizeCommandHandler.php new file mode 100644 index 0000000..ee32566 --- /dev/null +++ b/src/MessageHandler/Command/User/ServiceRequest/TryAutoFinalizeCommandHandler.php @@ -0,0 +1,37 @@ +serviceRequestRepository->get($message->requestServiceId); + if ($this->serviceRequestStatusWorkflow->canAutoFinalize($serviceRequest)) { + $this->serviceRequestStatusWorkflow->autoFinalize($serviceRequest); + $this->serviceRequestManager->save($serviceRequest, true); + } + } +} diff --git a/src/MessageHandler/Command/User/UpdateAddressCommandHandler.php b/src/MessageHandler/Command/User/UpdateAddressCommandHandler.php new file mode 100644 index 0000000..cd48c34 --- /dev/null +++ b/src/MessageHandler/Command/User/UpdateAddressCommandHandler.php @@ -0,0 +1,50 @@ +userRepository->find($message->id); + Assert::isInstanceOf($user, User::class); + + // we keep address, addressSupplement, and country from user input + $currentUserAddress = $user->getAddress(); + + // new address, then take the DTO which is already good + if ($currentUserAddress === null) { + $currentUserAddress = $message->userAddress; + } else { + // otherwise override the properties of the old address + $currentUserAddress->setFromAddressUpdateStep1($message->userAddress); + } + + // we take other properties from the Geocoder address + $this->adapter->fill($currentUserAddress, $message->newAddress); + + $user->setAddress($currentUserAddress); // we must set if it's a new address + $this->userManager->save($user, true); + } +} diff --git a/src/MessageHandler/Query/Admin/ParametersFormQueryHandler.php b/src/MessageHandler/Query/Admin/ParametersFormQueryHandler.php new file mode 100644 index 0000000..880999b --- /dev/null +++ b/src/MessageHandler/Query/Admin/ParametersFormQueryHandler.php @@ -0,0 +1,28 @@ +configurationRepository->getInstanceConfigurationOrCreate(); + $parametersForm = (new ParametersFormCommand()); + $parametersForm->hydrate($cfg); + + return $parametersForm; + } +} diff --git a/src/MessageHandler/Query/Group/GetGroupByIdQueryHandler.php b/src/MessageHandler/Query/Group/GetGroupByIdQueryHandler.php new file mode 100644 index 0000000..59b54fc --- /dev/null +++ b/src/MessageHandler/Query/Group/GetGroupByIdQueryHandler.php @@ -0,0 +1,30 @@ +groupRepository->find($message->id); + if ($group === null) { + throw new GroupNotFoundException($message->id); + } + + return $group; + } +} diff --git a/src/MessageHandler/Query/Group/GetGroupMembersHandler.php b/src/MessageHandler/Query/Group/GetGroupMembersHandler.php new file mode 100644 index 0000000..4db684e --- /dev/null +++ b/src/MessageHandler/Query/Group/GetGroupMembersHandler.php @@ -0,0 +1,28 @@ +groupRepository->get($message->id); + + return $this->userGroupRepository->getGroupMembers($group, $message->memberName); + } +} diff --git a/src/MessageHandler/Query/Group/GetGroupsQueryHandler.php b/src/MessageHandler/Query/Group/GetGroupsQueryHandler.php new file mode 100644 index 0000000..63fc722 --- /dev/null +++ b/src/MessageHandler/Query/Group/GetGroupsQueryHandler.php @@ -0,0 +1,24 @@ +groupRepository->getGroups($message->groupName); + } +} diff --git a/src/MessageHandler/Query/Product/GetProductByIdQueryHandler.php b/src/MessageHandler/Query/Product/GetProductByIdQueryHandler.php new file mode 100644 index 0000000..2b85eea --- /dev/null +++ b/src/MessageHandler/Query/Product/GetProductByIdQueryHandler.php @@ -0,0 +1,40 @@ +productRepository->find($message->id); + if ($product === null) { + throw new ProductNotFoundException($message->id); + } + + if ($message->attribute !== null && !$this->security->isGranted($message->attribute, $product)) { + throw new AccessDeniedHttpException("Access to product {$product->getId()} and attribute $message->attribute denied."); + } + + return $product; + } +} diff --git a/src/MessageHandler/Query/Product/GetProductUnavailabilitiesQueryHandler.php b/src/MessageHandler/Query/Product/GetProductUnavailabilitiesQueryHandler.php new file mode 100644 index 0000000..b98ccf3 --- /dev/null +++ b/src/MessageHandler/Query/Product/GetProductUnavailabilitiesQueryHandler.php @@ -0,0 +1,31 @@ + + */ + public function __invoke(GetProductUnavailabilitiesQuery $message): array + { + $product = $this->productRepository->get($message->id); + + return $this->productAvailabilityRepository->getProductUnavailabilities($product); + } +} diff --git a/src/MessageHandler/Query/Security/GetUserByTokenQueryHandler.php b/src/MessageHandler/Query/Security/GetUserByTokenQueryHandler.php new file mode 100644 index 0000000..00b3f73 --- /dev/null +++ b/src/MessageHandler/Query/Security/GetUserByTokenQueryHandler.php @@ -0,0 +1,41 @@ +userRepository->findOneByConfirmationToken($message->token); + + if ($user === null) { + throw new UserNotFoundException($message->token); + } + + if ($user->isConfirmationTokenExpired($this->clock->now())) { + throw new UserConfirmationTokenExpiredException($user->getId()); + } + + return $user; + } +} diff --git a/src/MessageHandler/Query/Security/ResetPasswordQueryHandler.php b/src/MessageHandler/Query/Security/ResetPasswordQueryHandler.php new file mode 100644 index 0000000..61fb75d --- /dev/null +++ b/src/MessageHandler/Query/Security/ResetPasswordQueryHandler.php @@ -0,0 +1,41 @@ +userRepository->findOneByLostPasswordToken($message->token); + + if ($user === null) { + throw new UserNotFoundException($message->token); + } + + if ($user->isLostPasswordTokenExpired($this->clock->now())) { + throw new UserLostPasswordTokenExpiredException(); + } + + return $user; + } +} diff --git a/src/MessageHandler/Query/ServiceRequest/GetLendingsQueryHandler.php b/src/MessageHandler/Query/ServiceRequest/GetLendingsQueryHandler.php new file mode 100644 index 0000000..7340eb2 --- /dev/null +++ b/src/MessageHandler/Query/ServiceRequest/GetLendingsQueryHandler.php @@ -0,0 +1,28 @@ +userRepository->get($message->userId); + + return $this->serviceRequestRepository->getLendings($user, $message->products); + } +} diff --git a/src/MessageHandler/Query/ServiceRequest/GetLoansQueryHandler.php b/src/MessageHandler/Query/ServiceRequest/GetLoansQueryHandler.php new file mode 100644 index 0000000..6000366 --- /dev/null +++ b/src/MessageHandler/Query/ServiceRequest/GetLoansQueryHandler.php @@ -0,0 +1,28 @@ +userRepository->get($message->userId); + + return $this->serviceRequestRepository->getLoans($user, $message->products); + } +} diff --git a/src/MessageHandler/Query/ServiceRequest/GetServiceRequestByIdQueryHandler.php b/src/MessageHandler/Query/ServiceRequest/GetServiceRequestByIdQueryHandler.php new file mode 100644 index 0000000..386e2a2 --- /dev/null +++ b/src/MessageHandler/Query/ServiceRequest/GetServiceRequestByIdQueryHandler.php @@ -0,0 +1,41 @@ +serviceRequestRepository->find($message->id); + if ($serviceRequest === null) { + throw new ServiceRequestNotFoundException($message->id); + } + + if (!$this->security->isGranted(ServiceRequestVoter::VIEW, $serviceRequest)) { + throw new AccessDeniedException(sprintf('Access to service request "%s" denied (not owner or recipient).', $message->id)); + } + + return $serviceRequest; + } +} diff --git a/src/MessageHandler/Query/User/Account/GetUserQueryHandler.php b/src/MessageHandler/Query/User/Account/GetUserQueryHandler.php new file mode 100644 index 0000000..4766e22 --- /dev/null +++ b/src/MessageHandler/Query/User/Account/GetUserQueryHandler.php @@ -0,0 +1,24 @@ +userRepository->get($message->id); + } +} diff --git a/src/MessageHandler/Query/User/GetUserObjectsQueryHandler.php b/src/MessageHandler/Query/User/GetUserObjectsQueryHandler.php new file mode 100644 index 0000000..3666ba1 --- /dev/null +++ b/src/MessageHandler/Query/User/GetUserObjectsQueryHandler.php @@ -0,0 +1,32 @@ +userRepository->find($message->id); + Assert::isInstanceOf($user, User::class); + + return $this->productRepository->getUserProductsByType($user, ProductType::OBJECT, $message->categoryId, null); + } +} diff --git a/src/MessageHandler/Query/User/GetUserServicesQueryHandler.php b/src/MessageHandler/Query/User/GetUserServicesQueryHandler.php new file mode 100644 index 0000000..f5e05f2 --- /dev/null +++ b/src/MessageHandler/Query/User/GetUserServicesQueryHandler.php @@ -0,0 +1,32 @@ +userRepository->find($message->id); + Assert::isInstanceOf($user, User::class); + + return $this->productRepository->getUserProductsByType($user, ProductType::SERVICE, $message->categoryId, null); + } +} diff --git a/src/MessageHandler/Query/User/UserAddressQueryHandler.php b/src/MessageHandler/Query/User/UserAddressQueryHandler.php new file mode 100644 index 0000000..1e5e981 --- /dev/null +++ b/src/MessageHandler/Query/User/UserAddressQueryHandler.php @@ -0,0 +1,32 @@ +geoProvider->getAddressCollection( + $message->getAddressForQuery(), + self::WITH_LIMIT, + ); + } +} diff --git a/src/Notifier/SmsNotifier.php b/src/Notifier/SmsNotifier.php new file mode 100644 index 0000000..6733658 --- /dev/null +++ b/src/Notifier/SmsNotifier.php @@ -0,0 +1,58 @@ +canBeNotifiedBySms()) { + return null; + } + + $phoneNumber = $user->getPhoneNumber(); + Assert::notEmpty($phoneNumber); + Assert::notEmpty($subject); + + // fail silently, it should not happen as the number is validated in the form. + // We want to avoid a 500 error from the vendors + if (!u($phoneNumber)->startsWith('+')) { + $this->logger->warning('Invalid phone number: '.$phoneNumber); + + return null; + } + + try { + return $this->texter->send(new SmsMessage( + phone: $phoneNumber, + subject: $subject + )); + } catch (\Exception $e) { + // OK, the sms cannot be delivered, but this is not critical as the an + // email is always sent + $this->logger->warning('Cannot deliver text message: '.$e->getMessage()); + + return null; + } + } +} diff --git a/src/Notifier/SmsNotifierTrait.php b/src/Notifier/SmsNotifierTrait.php new file mode 100644 index 0000000..5c7d36a --- /dev/null +++ b/src/Notifier/SmsNotifierTrait.php @@ -0,0 +1,30 @@ + $subjectContext addtional context for the subject + */ + private function sendSms(User $user, string $emailClass, array $subjectContext = []): void + { + $i18nPrefix = $this->getI18nPrefix($emailClass); + $subject = $this->translator->trans($i18nPrefix.'.subject', array_merge(['%brand%' => $this->brand], $subjectContext), AppMailer::TR_DOMAIN); + $this->notifier->notify( + $user, + $subject, + ); + } +} diff --git a/src/Payment/PayumManager.php b/src/Payment/PayumManager.php new file mode 100644 index 0000000..a7b4bbc --- /dev/null +++ b/src/Payment/PayumManager.php @@ -0,0 +1,90 @@ +payum->getStorage(Payment::class); + + /** @var Payment $payment */ + $payment = $storage->create(); + $payment->setNumber(uniqid('payum_', true)); + $payment->setCurrencyCode($groupOffer->getCurrency()); + $payment->setTotalAmount($groupOffer->getPrice()); + $payment->setDescription($groupOffer->getGroup()->getName().' / '.$groupOffer->getName()); + $payment->setClientId((string) $user->getId()); + $payment->setClientEmail($user->getEmail()); + $payment->setUser($user); + $payment->setDetails($this->getGatewayDetails($groupOffer)); + $storage->update($payment); + + return $payment; + } + + /** + * Add specific details to the current gateway. Put here any fields in a gateway format. + * For now this function is specific to Mollie. + * + * For example if you use Paypal ExpressCheckout you can define a description of the first item: + * 'L_PAYMENTREQUEST_0_DESC0' => 'A desc' + * + * @todo Check if the default method can be retrieved from the gateway configuration. + * + * @see https://github.com/webbaard/payum-mollie/blob/master/Resources/doc/checkout_mollie.md + * + * @return array + */ + private function getGatewayDetails(GroupOffer $groupOffer): array + { + return [ + // method must be set as the default value is not retrieved from the gateway configuration + 'method' => PaymentMethod::CREDITCARD->value, + 'metadata' => [ + 'groupId' => (string) $groupOffer->getGroup()->getId(), + 'groupOfferId' => (string) $groupOffer->getId(), + ], + ]; + } + + /** + * @param array $afterParameters + * + * @see PrepareAction + */ + public function getCaptureToken(Payment $payment, string $afterRoute, array $afterParameters): TokenInterface + { + return $this->payum->getTokenFactory()->createCaptureToken( + $this->payumGateway, + $payment, + $afterRoute, + $afterParameters + ); + } +} diff --git a/src/Repository/.gitignore b/src/Repository/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/Repository/AddressRepository.php b/src/Repository/AddressRepository.php new file mode 100644 index 0000000..9af156d --- /dev/null +++ b/src/Repository/AddressRepository.php @@ -0,0 +1,27 @@ + + * + * @method Address|null find($id, $lockMode = null, $lockVersion = null) + * @method Address|null findOneBy(array $criteria, array $orderBy = null) + * @method Address[] findAll() + * @method Address[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +final class AddressRepository extends ServiceEntityRepository +{ + private const ENTITY_CLASS = Address::class; + + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, self::ENTITY_CLASS); + } +} diff --git a/src/Repository/CategoryRepository.php b/src/Repository/CategoryRepository.php new file mode 100644 index 0000000..98f82c7 --- /dev/null +++ b/src/Repository/CategoryRepository.php @@ -0,0 +1,89 @@ +. + * + * @method Category|null find($id, $lockMode = null, $lockVersion = null) + * @method Category|null findOneBy(array $criteria, array $orderBy = null) + * @method Category[] findAll() + * @method Category[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +final class CategoryRepository extends NestedTreeRepository implements ServiceEntityRepositoryInterface +{ + private const ENTITY_CLASS = Category::class; + + public function __construct(ManagerRegistry $registry) + { + /** @var EntityManagerInterface $manager */ + $manager = $registry->getManagerForClass(self::ENTITY_CLASS); + parent::__construct($manager, $manager->getClassMetadata(self::ENTITY_CLASS)); + } + + public function save(Category $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(Category $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function addTypeFilter(QueryBuilder $qb, ProductType $productType): QueryBuilder + { + return $qb + ->andWhere('entity.type = :type') + ->setParameter(':type', $productType); + } + + /** + * Get the category hierarchy thanks to the tree behaviour. Ordering by the + * left index is the easiest way to do the job. + */ + public function getHierarchy(?ProductType $type = null, ?User $user = null): QueryBuilder + { + $qb = $this->createQueryBuilder('c') + ->andWhere('c.enabled = true') + ->addOrderBy('c.lft', 'ASC'); + + if ($type !== null) { + $qb + ->andWhere('c.type = :productType') + ->setParameter('productType', $type) + ; + } + + if ($user !== null) { + $qb->from(Product::class, 'p') + ->andWhere('p.category = c') + ->andWhere('p.owner = :user') + ->setParameter('user', $user) + ; + } + + return $qb; + } +} diff --git a/src/Repository/ConfigurationRepository.php b/src/Repository/ConfigurationRepository.php new file mode 100644 index 0000000..3826420 --- /dev/null +++ b/src/Repository/ConfigurationRepository.php @@ -0,0 +1,61 @@ + + * + * @method Configuration|null find($id, $lockMode = null, $lockVersion = null) + * @method Configuration|null findOneBy(array $criteria, array $orderBy = null) + * @method Configuration[] findAll() + * @method Configuration[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +final class ConfigurationRepository extends ServiceEntityRepository +{ + private const ENTITY_CLASS = Configuration::class; + + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, self::ENTITY_CLASS); + } + + public function save(Configuration $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(Configuration $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function getInstanceConfiguration(): ?Configuration + { + return $this->findOneBy(['type' => ConfigurationType::INSTANCE]); + } + + public function getInstanceConfigurationOrCreate(): Configuration + { + $cfg = $this->getInstanceConfiguration(); + if (!$cfg instanceof Configuration) { + $cfg = Configuration::getInstanceConfiguration(); + } + + return $cfg; + } +} diff --git a/src/Repository/GroupOfferRepository.php b/src/Repository/GroupOfferRepository.php new file mode 100644 index 0000000..cda327c --- /dev/null +++ b/src/Repository/GroupOfferRepository.php @@ -0,0 +1,35 @@ + + * + * @method GroupOffer|null find($id, $lockMode = null, $lockVersion = null) + * @method GroupOffer|null findOneBy(array $criteria, array $orderBy = null) + * @method GroupOffer[] findAll() + * @method GroupOffer[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class GroupOfferRepository extends ServiceEntityRepository +{ + private const ENTITY_CLASS = GroupOffer::class; + + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, self::ENTITY_CLASS); + } + + /** + * Return an object or throws an exception if not found. + */ + public function get(mixed $id, int|null $lockMode = null, int|null $lockVersion = null): GroupOffer + { + return $this->find($id, $lockMode, $lockVersion) ?? throw new \LogicException('Group offer not found.'); + } +} diff --git a/src/Repository/GroupRepository.php b/src/Repository/GroupRepository.php new file mode 100644 index 0000000..65a01b6 --- /dev/null +++ b/src/Repository/GroupRepository.php @@ -0,0 +1,90 @@ + + * + * @method Group|null find($id, $lockMode = null, $lockVersion = null) + * @method Group|null findOneBy(array $criteria, array $orderBy = null) + * @method Group[] findAll() + * @method Group[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +final class GroupRepository extends ServiceEntityRepository +{ + private const ENTITY_CLASS = Group::class; + + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, self::ENTITY_CLASS); + } + + /** + * Return an object or throws an exception if not found. + */ + public function get(mixed $id, int|null $lockMode = null, int|null $lockVersion = null): Group + { + return $this->find($id, $lockMode, $lockVersion) ?? throw new \LogicException('Group not found.'); + } + + public function save(Group $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(Group $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * Get visible groups for a given user. + */ + public function getGroups(?string $groupName): Query + { + $qb = $this + ->createQueryBuilder('g') + + // public + ->andWhere('g.type = :type') + ->setParameter(':type', GroupType::PUBLIC); + + // @todo or member of the private group + + // filter list by group name + if ($groupName !== null) { + $qb->andWhere('LOWER(g.name) LIKE LOWER(:groupName)')->setParameter('groupName', '%'.$groupName.'%'); + } + + // alpha sort + return $qb->orderBy('g.name', 'ASC')->getQuery(); + } + + public function getUserGroups(User $user): QueryBuilder + { + return $this->createQueryBuilder('g') + ->from(UserGroup::class, 'ug') + ->andWhere('g = ug.group') + ->andWhere('ug.user = :user') + ->setParameter('user', $user); + } +} diff --git a/src/Repository/MenuItemRepository.php b/src/Repository/MenuItemRepository.php new file mode 100644 index 0000000..94d9286 --- /dev/null +++ b/src/Repository/MenuItemRepository.php @@ -0,0 +1,94 @@ +getClassMetadata(self::ENTITY_CLASS)); + } + + public function save(MenuItem $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(MenuItem $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * @return MenuItem[] + */ + public function findFirstLevelMenuLinks(string $code): array + { + /** @var MenuItem[] */ + return $this + ->createQueryBuilder('i') + ->andWhere('i.parent is null') + ->join('i.menu', 'm', 'WITH', 'm.code = :code') + ->setParameter('code', $code) + ->orderBy('i.position', 'ASC') + ->getQuery() + ->getResult(); + } + + /** + * @return MenuItem[] + */ + public function getFooterItems(string $linkType): array + { + /** @var MenuItem[] */ + return $this + ->createQueryBuilder('i') + ->andWhere('i.linkType = :linkType') + ->join('i.menu', 'm', 'WITH', 'm.code = :code') + ->setParameters([ + 'code' => Menu::FOOTER, + 'linkType' => $linkType, + ]) + ->orderBy('i.position', 'ASC') + ->getQuery() + ->getResult(); + } + + public function getLinksByCode(QueryBuilder $qb, string $code): QueryBuilder + { + return $qb + ->join('entity.menu', 'm', 'WITH', 'm.code = :code') + ->andWhere('entity.linkType = :linkType') + ->andWhere('entity.parent is null') + ->setParameters([ + 'code' => $code, + 'linkType' => LinkType::LINK->value, + ]) + ; + } +} diff --git a/src/Repository/MenuRepository.php b/src/Repository/MenuRepository.php new file mode 100644 index 0000000..15a3c7a --- /dev/null +++ b/src/Repository/MenuRepository.php @@ -0,0 +1,61 @@ + + * + * @method Menu|null find($id, $lockMode = null, $lockVersion = null) + * @method Menu|null findOneBy(array $criteria, array $orderBy = null) + * @method Menu[] findAll() + * @method Menu[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +final class MenuRepository extends ServiceEntityRepository +{ + private const ENTITY_CLASS = Menu::class; + + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, self::ENTITY_CLASS); + } + + public function save(Menu $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(Menu $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * @throws NonUniqueResultException + */ + public function getByCode(string $code): Menu + { + /** @var Menu */ + return $this + ->createQueryBuilder('m') + ->where('m.code = :code') + ->setParameter('code', $code) + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(); + } +} diff --git a/src/Repository/MessageRepository.php b/src/Repository/MessageRepository.php new file mode 100644 index 0000000..8b9d6a4 --- /dev/null +++ b/src/Repository/MessageRepository.php @@ -0,0 +1,69 @@ + + * + * @method Message|null find($id, $lockMode = null, $lockVersion = null) + * @method Message|null findOneBy(array $criteria, array $orderBy = null) + * @method Message[] findAll() + * @method Message[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +final class MessageRepository extends ServiceEntityRepository +{ + private const ENTITY_CLASS = Message::class; + + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, self::ENTITY_CLASS); + } + + public function save(Message $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(Message $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function userHasNewMessage(User $user, bool $isOwner): bool + { + $qb = $this + ->createQueryBuilder('m') + ->join('m.serviceRequest', 'sr'); + + if ($isOwner) { + $qb->andWhere('m.ownerRead = false') + ->andWhere('sr.owner = :user') + ->setParameter('user', $user); + } else { + $qb + ->andWhere('m.recipientRead = false') + ->andWhere('sr.recipient = :user') + ->setParameter('user', $user); + } + + /** @var Message[] $unreadMessages */ + $unreadMessages = $qb->getQuery()->getResult(); + + return \count($unreadMessages) !== 0; + } +} diff --git a/src/Repository/PageRepository.php b/src/Repository/PageRepository.php new file mode 100644 index 0000000..9f51aff --- /dev/null +++ b/src/Repository/PageRepository.php @@ -0,0 +1,33 @@ + + * + * @method Page|null find($id, $lockMode = null, $lockVersion = null) + * @method Page|null findOneBy(array $criteria, array $orderBy = null) + * @method Page|null findOneBySlug(string $slug) + * @method Page[] findAll() + * @method Page[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +final class PageRepository extends ServiceEntityRepository +{ + private const ENTITY_CLASS = Page::class; + + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, self::ENTITY_CLASS); + } + + public function getHome(): ?Page + { + return $this->findOneBy(['home' => true]); + } +} diff --git a/src/Repository/PaymentRepository.php b/src/Repository/PaymentRepository.php new file mode 100644 index 0000000..d9f26c7 --- /dev/null +++ b/src/Repository/PaymentRepository.php @@ -0,0 +1,32 @@ + + * + * @method Payment|null find($id, $lockMode = null, $lockVersion = null) + * @method Payment|null findOneBy(array $criteria, array $orderBy = null) + * @method Payment[] findAll() + * @method Payment[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +final class PaymentRepository extends ServiceEntityRepository +{ + private const ENTITY_CLASS = Payment::class; + + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, self::ENTITY_CLASS); + } + + public function get(mixed $id, int|null $lockMode = null, int|null $lockVersion = null): Payment + { + return $this->find($id, $lockMode, $lockVersion) ?? throw new \LogicException('Payment not found.'); + } +} diff --git a/src/Repository/ProductAvailabilityRepository.php b/src/Repository/ProductAvailabilityRepository.php new file mode 100644 index 0000000..8d7af14 --- /dev/null +++ b/src/Repository/ProductAvailabilityRepository.php @@ -0,0 +1,76 @@ + + * + * @method ProductAvailability|null find($id, $lockMode = null, $lockVersion = null) + * @method ProductAvailability|null findOneBy(array $criteria, array $orderBy = null) + * @method ProductAvailability[] findAll() + * @method ProductAvailability[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +final class ProductAvailabilityRepository extends ServiceEntityRepository +{ + private const ENTITY_CLASS = ProductAvailability::class; + + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, self::ENTITY_CLASS); + } + + /** + * Return product availability or throws an exception if not found. + */ + public function get(mixed $id, int|null $lockMode = null, int|null $lockVersion = null): ProductAvailability + { + return $this->find($id, $lockMode, $lockVersion) ?? throw new \LogicException('ProductAvailability not found.'); + } + + public function save(ProductAvailability $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * @return array + */ + public function getProductUnavailabilities(Product $product): array + { + /** @var ProductAvailability[] */ + return $this + ->createQueryBuilder('a') + ->andWhere('a.product = :product') + ->andWhere('a.mode = :mode') + ->andWhere('a.type = :type') + ->andWhere('a.endAt > :today') + ->orderBy('a.startAt') + ->setParameters([ + 'product' => $product, + 'mode' => ProductAvailabilityMode::UNAVAILABLE, + 'type' => ProductAvailabilityType::OWNER, + 'today' => date('Y-m-d'), + ]) + ->getQuery() + ->getResult(); + } + + public function deleteProductUnavailability(ProductAvailability $productAvailability): void + { + $this->getEntityManager()->remove($productAvailability); + $this->getEntityManager()->flush(); + } +} diff --git a/src/Repository/ProductRepository.php b/src/Repository/ProductRepository.php new file mode 100644 index 0000000..f61bebd --- /dev/null +++ b/src/Repository/ProductRepository.php @@ -0,0 +1,137 @@ + + * + * @method Product|null find($id, $lockMode = null, $lockVersion = null) + * @method Product|null findOneBy(array $criteria, array $orderBy = null) + * @method Product[] findAll() + * @method Product[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class ProductRepository extends ServiceEntityRepository +{ + private const ENTITY_CLASS = Product::class; + + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, self::ENTITY_CLASS); + } + + /** + * Return an object or throws an exception if not found. + */ + public function get(mixed $id, int|null $lockMode = null, int|null $lockVersion = null): Product + { + return $this->find($id, $lockMode, $lockVersion) ?? throw new \LogicException('Product not found.'); + } + + public function save(Product $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(Product $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function getUserProductsByType(User $user, ?ProductType $type, ?Uuid $category, ?Group $group): Query + { + $qb = $this + ->createQueryBuilder('p') + ->andWhere('p.owner = :user') + ->andWhere('p.status != :status') + ->setParameters([ + 'user' => $user, + 'status' => ProductStatus::DELETED, + ]); + + if ($type !== null) { + $qb->andWhere('p.type = :type') + ->setParameter('type', $type) + ; + } + + if ($category !== null) { + $qb->andWhere('p.category = :category') + ->setParameter('category', $category) + ; + } + + if ($group !== null) { + $qb->innerJoin('p.groups', 'g') + ->andWhere('g.id = :group') + ->setParameter('group', $group) + ; + } + + return $qb->getQuery(); + } + + /** + * Business rules for searchable/indexable products. + */ + public function getIndexable(?ProductType $type = null): Query + { + $qb = $this + ->createQueryBuilder('p') + ->innerJoin('p.owner', 'owner') + + // enabled and confirmed accounts + ->andWhere('owner.enabled = :enabled') + ->setParameter(':enabled', true) + ->andWhere('owner.emailConfirmed = :emailConfirmed') + ->setParameter(':emailConfirmed', true) + + // vacation mode is not enabled for owner + ->andWhere('owner.vacationMode = :vacationMode') + ->setParameter(':vacationMode', false) + + // active products + ->andWhere('p.status = :status') + ->setParameter(':status', ProductStatus::ACTIVE) + + // alpha sort + ->orderBy('p.name', 'ASC') + ; + + if ($type !== null) { + $qb->andWhere('p.type = :type') + ->setParameter(':type', $type); + } + + return $qb->getQuery(); + } + + public function getObjects(): Query + { + return $this->getIndexable(ProductType::OBJECT); + } + + public function getServices(): Query + { + return $this->getIndexable(ProductType::SERVICE); + } +} diff --git a/src/Repository/ServiceRequestRepository.php b/src/Repository/ServiceRequestRepository.php new file mode 100644 index 0000000..207c63b --- /dev/null +++ b/src/Repository/ServiceRequestRepository.php @@ -0,0 +1,129 @@ + + * + * @method ServiceRequest|null find($id, $lockMode = null, $lockVersion = null) + * @method ServiceRequest|null findOneBy(array $criteria, array $orderBy = null) + * @method ServiceRequest[] findAll() + * @method ServiceRequest[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +final class ServiceRequestRepository extends ServiceEntityRepository +{ + private const ENTITY_CLASS = ServiceRequest::class; + + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, self::ENTITY_CLASS); + } + + /** + * Return an object or throws an exception if not found. + */ + public function get(mixed $id, int|null $lockMode = null, int|null $lockVersion = null): ServiceRequest + { + return $this->find($id, $lockMode, $lockVersion) ?? throw new \LogicException('Service request not found.'); + } + + public function save(ServiceRequest $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(ServiceRequest $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * @param array|ArrayCollection|null $products + */ + public function getLendings(User $owner, mixed $products): Query + { + $qb = $this + ->createQueryBuilder('sr') + ->leftJoin('sr.messages', 'm') + ->andWhere('sr.owner = :owner') + ->setParameter('owner', $owner) + ->orderBy('sr.createdAt', 'DESC'); + + if ($products !== null && \count($products) !== 0) { + $qb->andWhere('sr.product IN (:products)')->setParameter('products', $products); + } + + return $qb->getQuery(); + } + + /** + * @param array|ArrayCollection|null $products + */ + public function getLoans(User $recipient, mixed $products): Query + { + $qb = $this + ->createQueryBuilder('sr') + ->leftJoin('sr.messages', 'm') + ->andWhere('sr.recipient = :recipient') + ->setParameter('recipient', $recipient) + ->orderBy('sr.createdAt', 'DESC'); + + if ($products !== null && \count($products) !== 0) { + $qb->andWhere('sr.product IN (:products)')->setParameter('products', $products); + } + + return $qb->getQuery(); + } + + /** + * Get all items having a property set to a given date interval (a day). + */ + public function getActionSoon(string $property, int $days = 1): Query + { + $from = new \DateTimeImmutable(sprintf('+%d days midnight', $days)); + $to = $from->modify('+ 1 day'); // just add one day for the end limit + + $qb = $this + ->createQueryBuilder('sr') + ->innerJoin('sr.owner', 'o') + ->innerJoin('sr.recipient', 'g') + ->andWhere(sprintf('sr.%s >= :from', $property)) + ->andWhere(sprintf('sr.%s < :to', $property)) + ->setParameter('from', $from->format('Y-m-d')) + ->setParameter('to', $to->format('Y-m-d')) + ->andWhere('sr.status = :status') + ->setParameter('status', ServiceRequestStatus::CONFIRMED) + ; + + return $qb->getQuery(); + } + + public function getStartingAtTomorow(): Query + { + return $this->getActionSoon('startAt'); + } + + public function getEndingAtTomorow(): Query + { + return $this->getActionSoon('endAt'); + } +} diff --git a/src/Repository/UserGroupRepository.php b/src/Repository/UserGroupRepository.php new file mode 100644 index 0000000..1db29dc --- /dev/null +++ b/src/Repository/UserGroupRepository.php @@ -0,0 +1,104 @@ + + * + * @method UserGroup|null find($id, $lockMode = null, $lockVersion = null) + * @method UserGroup|null findOneBy(array $criteria, array $orderBy = null) + * @method UserGroup[] findAll() + * @method UserGroup[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class UserGroupRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, UserGroup::class); + } + + public function save(UserGroup $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(UserGroup $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function getGroupMembers(Group $group, ?string $memberName): Query + { + $qb = $this + ->createQueryBuilder('ug') + ->andWhere('ug.group = :group') + ->join('ug.user', 'u') + ->orderBy('u.firstname', 'ASC') + ->setParameter('group', $group); + + // filtered list by firstname, lastname or email member. + if ($memberName !== null) { + $qb + ->andWhere( + 'LOWER(u.email) LIKE LOWER(:memberName) + OR LOWER(u.firstname) LIKE LOWER(:memberName) + OR LOWER(u.lastname) LIKE LOWER(:memberName)' + ) + ->setParameter('memberName', '%'.$memberName.'%'); + } + + return $qb->getQuery(); + } + + public function getExpired(): Query + { + $today = Carbon::today(); + $qb = $this + ->createQueryBuilder('ug') + ->join('ug.user', 'u') + ->join('ug.group', 'g') + ->andWhere('ug.endAt < :date') + ->setParameter('date', $today->format('Y-m-d')) + ; + + return $qb->getQuery(); + } + + /** + * Get all membership expiring in exactly x days. + */ + public function getExpiring(int $days): Query + { + $from = new \DateTimeImmutable(sprintf('+%d days midnight', $days)); + $to = $from->modify('+ 1 day'); // just add one day for the end limit + + $qb = $this + ->createQueryBuilder('ug') + ->join('ug.user', 'u') + ->join('ug.group', 'g') + ->andWhere('ug.endAt >= :from') + ->andWhere('ug.endAt < :to') + ->setParameter('from', $from->format('Y-m-d')) + ->setParameter('to', $to->format('Y-m-d')) + ; + + return $qb->getQuery(); + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..cfc22d8 --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,108 @@ + + * + * @method User|null find($id, $lockMode = null, $lockVersion = null) + * @method User|null findOneBy(array $criteria, array $orderBy = null) + * @method User|null findOneByEmail(string $email) + * @method User|null findOneByConfirmationToken(string $token) + * @method User|null findOneByLostPasswordToken(string $token) + * @method User[] findAll() + * @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + /** + * Return object or throws an exception if not found. + */ + public function get(mixed $id, int|null $lockMode = null, int|null $lockVersion = null): User + { + return $this->find($id, $lockMode, $lockVersion) ?? throw new \LogicException('User not found.'); + } + + /** + * Use the UserManager instead. + */ + public function save(User $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * Use the UserManager instead. + */ + public function remove(User $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + /** + * Used to upgrade (rehash) the user's password automatically over time. + */ + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + if (!$user instanceof User) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user))); + } + + $user->setPassword($newHashedPassword); + $this->save($user, true); + } + + /** + * This rules are duplicated with ProductRepository::getIndexable. Check to + * factorize this. + * + * @see ProductRepository::getIndexable + */ + public function getPlacesQueryBuilder(): QueryBuilder + { + return $this->createQueryBuilder('p') + ->andWhere('p.type = :type') + ->setParameter('type', UserType::PLACE) + + // enabled and confirmed accounts + ->andWhere('p.enabled = :enabled') + ->setParameter(':enabled', true) + ->andWhere('p.emailConfirmed = :emailConfirmed') + ->setParameter(':emailConfirmed', true) + + // vacation mode is not enabled + ->andWhere('p.vacationMode = :vacationMode') + ->setParameter(':vacationMode', false) + + // only places with a valid address + ->andWhere('p.address is not null') + + // sort + ->orderBy('p.name', 'ASC') + ; + } +} diff --git a/src/Search/Command/IndexProductsCommand.php b/src/Search/Command/IndexProductsCommand.php new file mode 100644 index 0000000..fa78fbd --- /dev/null +++ b/src/Search/Command/IndexProductsCommand.php @@ -0,0 +1,111 @@ +configureCommand(self::DESCRIPTION); + } + + /** + * @throws ExceptionInterface + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $io->title(self::DESCRIPTION.' ('.$this->environment.' env)'); + $this->memoryReport($io); + + $io->section('Resetting swap index...'); + $swapIndex = $this->meilisearch->getSwapIndex(); + $swapIndex->deleteAllDocuments(); + $io->note(' -> DONE'); + $io->newLine(); + + $io->section('Indexing products in swap...'); + $count = 0; + $toIndex = []; + // simple trick for code coverage as we don't have 500 products in the fixtures + $batchSize = $this->environment === 'test' ? 10 : self::BACTH_SIZE; + $query = $this->productRepository->getIndexable(); + + foreach ($query->toIterable() as $product) { + /** @var Product $product */ + $io->comment(sprintf(' > adding product %s to batch', $product->getId())); + $toIndex[] = $product; + if ((\count($toIndex) % $batchSize) === 0) { + $this->meilisearch->indexProducts($toIndex, $swapIndex); + $io->note(sprintf(' > indexing %d product(s) from batch', \count($toIndex))); + $toIndex = []; + } + ++$count; + } + + $this->meilisearch->indexProducts($toIndex, $swapIndex); + $io->note(sprintf(' > indexing %d remaining product(s) from batch', \count($toIndex))); + + $io->note(sprintf(' -> %d product(s) indexed.', $count)); + $io->note(' -> DONE'); + $io->newLine(); + + $io->section('Swapping indexes...'); + $this->meilisearch->swapIndexes(); + $io->note(' -> DONE'); + $io->newLine(); + + $io->section('Applying settings...'); + $this->meilisearch->setSettings(); + $io->note(' -> DONE'); + + $io->section('Resetting swap index...'); + $swapIndex = $this->meilisearch->getSwapIndex(); + $swapIndex->deleteAllDocuments(); + $io->note(' -> DONE'); + $io->newLine(); + + $this->memoryReport($io); + sleep(1); + $io->success('DONE'); + + return Command::SUCCESS; + } +} diff --git a/src/Search/Document/GeoDocument.php b/src/Search/Document/GeoDocument.php new file mode 100644 index 0000000..a0568ed --- /dev/null +++ b/src/Search/Document/GeoDocument.php @@ -0,0 +1,29 @@ +getLatitude(), + lng: (float) $address->getLongitude(), + ); + } +} diff --git a/src/Search/Document/ProductDocument.php b/src/Search/Document/ProductDocument.php new file mode 100644 index 0000000..de82794 --- /dev/null +++ b/src/Search/Document/ProductDocument.php @@ -0,0 +1,136 @@ + + */ + public readonly array $categories, + + /** + * Categories' IDs. + * + * @see Product::$category + * + * @var array + */ + public readonly array $categoriesIds, + + /** + * Groups where the product is visible. + * + * @see Product::$groups + * + * @var array + */ + public readonly array $groupsIds, + + /** + * Optional description. + * + * @see Product::$description + */ + public readonly ?string $description, + + /** + * Optional lat/long coordinates. + * + * @@see Product::$owner + */ + public readonly ?GeoDocument $_geo, + + /** + * When the document was indexed. + */ + public readonly \DateTimeImmutable $indexedAt, + ) { + } + + public static function fromProduct(Product $product): self + { + // categories + $category = $product->getCategory(); + // Assert::isInstanceOf($category, Category::class); // category is not nullable in db + $categoriesIds = []; + $categories = $categoriesIds; + $categories[] = $category->getName(); + $categoriesIds[] = (string) $category->getId(); + if ($category->hasParent()) { + Assert::notNull($category->getParent()); + $categories[] = $category->getParent()->getName(); + $categoriesIds[] = (string) $category->getParent()->getId(); + } + + // add geoloc + $owner = $product->getOwner(); + if ($owner->hasAddress()) { + Assert::notNull($owner->getAddress()); + $geo = GeoDocument::fromAddress($owner->getAddress()); + } + + return new self( + id: (string) $product->getId(), + ownerId: (string) $product->getOwner()->getId(), + type: $product->getType()->value, + visibility: $product->getVisibility()->value, + name: $product->getName(), + categories: $categories, + categoriesIds: $categoriesIds, + groupsIds: $product->getGroupsIds(), + description: $product->getDescription(), + _geo: $geo ?? null, + indexedAt: new \DateTimeImmutable() + ); + } +} diff --git a/src/Search/Meilisearch.php b/src/Search/Meilisearch.php new file mode 100755 index 0000000..ad94dcd --- /dev/null +++ b/src/Search/Meilisearch.php @@ -0,0 +1,320 @@ +client = new Client($this->meilisearchUrl, $this->meilisearchApiKey); + } + + /** + * For direct access to main Meili client. + */ + public function getClient(): Client + { + return $this->client; + } + + public function getIndex(): Indexes + { + if ($this->index !== null) { + return $this->index; + } + + $this->index = $this->client->index(self::PRODUCTS_INDEX); + + return $this->index; + } + + public function getSwapIndex(): Indexes + { + return $this->client->index(self::PRODUCTS_SWAP_INDEX); + } + + public function setSearchableAtttributes(): void + { + $this->getIndex()->updateSearchableAttributes(self::SEARCHABLE_ATTRIBUTES); + } + + public function setFiltrableAttributes(): void + { + $this->getIndex()->updateFilterableAttributes(self::FILTRABLE_ATTRIBUTES); + } + + public function setSortableAttributes(): void + { + $this->getIndex()->updateSortableAttributes(self::SORTABLE_ATTRIBUTES); + } + + /** + * Aplly all settings at once. + */ + public function setSettings(): void + { + $this->setSearchableAtttributes(); + $this->setFiltrableAttributes(); + $this->setSortableAttributes(); + } + + /** + * Allows to have full control about the normalization. But with tne 1.1 version + * of Meilisearch, we should be able to pass the document DTO as it is and let + * Meilisearch handle the normalization process. + * + * @return array + * + * @throws ExceptionInterface + */ + public function normalizeProduct(Product $product): array + { + $productDocument = ProductDocument::fromProduct($product); + /** @var array $normalized */ + $normalized = $this->normalizer->normalize($productDocument, 'array'); + + return $normalized; + } + + public function deleteProduct(Product $product, ?Indexes $index = null): void + { + $index = $index ?? $this->getIndex(); + $index->deleteDocument((string) $product->getId()); + } + + /** + * @throws ExceptionInterface + */ + public function indexProduct(Product $product, ?Indexes $index = null): void + { + $index = $index ?? $this->getIndex(); + $index->addDocuments([$this->normalizeProduct($product)], self::PRIMARY_KEY); + } + + /** + * @param array $products + * + * @throws ExceptionInterface + */ + public function indexProducts(array $products, ?Indexes $index = null): void + { + $index = $index ?? $this->getIndex(); + $documents = array_map(fn (Product $product) => $this->normalizeProduct($product), $products); + $index->addDocuments($documents, self::PRIMARY_KEY); + } + + /** + * Swap indexes to avoid downtime. + */ + public function swapIndexes(): void + { + $this->getClient()->swapIndexes([[self::PRODUCTS_INDEX, self::PRODUCTS_SWAP_INDEX]]); + } + + public function searchObjects(Search $searchDto): SearchResult + { + return $this->search($searchDto, ProductType::OBJECT); + } + + public function searchServices(Search $searchDto): SearchResult + { + return $this->search($searchDto, ProductType::SERVICE); + } + + /** + * Search with a main query and various filtery. + */ + public function search(Search $searchDto, ProductType $productType = null): SearchResult + { + $searchParams = []; + $searchParams = $this->withFilters($searchParams, $searchDto, $productType); + $searchParams = $this->withSort($searchParams, $searchDto); + + // pagination settings + $searchParams['hitsPerPage'] = ProductController::MAX_ELEMENT_BY_PAGE; + $searchParams['page'] = $searchDto->page; + + // option to transform hits to products while keeping the relevance order + $options = ['transformHits' => $this->transformHits(...)]; + + return $this->getIndex()->search($searchDto->q, $searchParams, $options); + } + + /** + * Apply all search filters. + * + * @param array $searchParams + * + * @return array + */ + private function withFilters(array $searchParams, Search $searchDto, ?ProductType $productType = null): array + { + $filters = []; + + // if the user is NOT logged then he can only view public products + // if the user is logged he will also view the products belonging to its groups + $visibilityFilter = []; + $visibilityFilter[] = 'visibility = '.ProductVisibility::PUBLIC->value; + if ($searchDto->isLogged()) { + Assert::isInstanceOf($searchDto->user, User::class); + $userGroupsIds = $searchDto->user->getUserGroupsIds(); + $visibilityFilter[] = 'groupsIds IN [ '.implode(', ', $userGroupsIds).' ]'; + } + $filters[] = '( '.implode(' OR ', $visibilityFilter).' )'; + + // product type as a filter + if ($productType !== null) { + $filters[] = 'type = '.$productType->value; + } + + // category filter + if ($searchDto->category !== null) { + $filters[] = 'categoriesIds = '.$searchDto->category->getId(); + } + + // place filter + if ($searchDto->place !== null) { + $filters[] = sprintf('ownerId = %s', $searchDto->place->getId()); + } + + // geo filter + if ($searchDto->hasProximity()) { + Assert::isInstanceOf($searchDto->city, Address::class); + $filters[] = sprintf('_geoRadius(%s, %s, %d)', + $searchDto->city->getLatitude(), + $searchDto->city->getLongitude(), + (int) $searchDto->distance * 1000 // the distance is in meters, not kilometers + ); + } + + // Filters are cumulative + $searchParams['filter'] = implode(' AND ', $filters); + + return $searchParams; + } + + /** + * Apply sort by name or proximity. + * + * @param array $searchParams + * + * @return array + */ + private function withSort(array $searchParams, Search $searchDto): array + { + // the proximity search has the priority to sort results + if ($searchDto->hasProximity()) { + Assert::isInstanceOf($searchDto->city, Address::class); + $searchParams['sort'] = [sprintf('_geoPoint(%s, %s):asc', + $searchDto->city->getLatitude(), + $searchDto->city->getLongitude()), + ]; + } + + // default sort: if no query is specified and not proximity filter then sort by name + if (!$searchDto->hasQuery() && !$searchDto->hasProximity()) { + $searchParams['sort'] = ['name:asc']; + } + + return $searchParams; + } + + /** + * Transform the hits to an array of product. If a product is not found is it + * simply removed from the results. + * + * @param array> $hits + * + * @return array + */ + private function transformHits(array $hits): array + { + $products = array_map($this->getProduct(...), $hits); + + return array_filter($products); + } + + /** + * @param array $hit + */ + private function getProduct(array $hit): ?Product + { + $product = $this->productRepository->find($hit['id'] ?? ''); // don't use null as it raises a doctrine exception + if ($product === null) { + return null; + } + + // enrich with the distance to the geoPoint if it is available + if (\array_key_exists('_geoDistance', $hit)) { + $product->setGeoDistance(\is_int($hit['_geoDistance']) ? $hit['_geoDistance'] : null); + } + + return $product; + } +} diff --git a/src/Search/Subscriber/SearchResultSubscriber.php b/src/Search/Subscriber/SearchResultSubscriber.php new file mode 100644 index 0000000..5d82f01 --- /dev/null +++ b/src/Search/Subscriber/SearchResultSubscriber.php @@ -0,0 +1,34 @@ +target; + if (!$searchResult instanceof SearchResult) { + return; + } + + $event->count = (int) $searchResult->getTotalHits(); + $event->items = $searchResult->getHits(); + $event->stopPropagation(); + } + + public static function getSubscribedEvents(): array + { + return [ + 'knp_pager.items' => ['items', 1/* increased priority to override any internal */], + ]; + } +} diff --git a/src/Security/Checker/AuthorizationChecker.php b/src/Security/Checker/AuthorizationChecker.php new file mode 100644 index 0000000..a64b6e9 --- /dev/null +++ b/src/Security/Checker/AuthorizationChecker.php @@ -0,0 +1,45 @@ +authorizationChecker->isGranted(User::ROLE_ADMIN); + } + + /** + * Check if the current logged user has the ADMIN role and throw an execption + * otherwise. + */ + public function checkAdminRole(): void + { + if (!$this->isAdmin()) { + throw new AccessDeniedHttpException('Admin role is required to access this ressource.'); + } + } + + public function hasGroupAdminRole(): bool + { + return $this->authorizationChecker->isGranted(User::ROLE_GROUP_ADMIN); + } + + public function isGroupAdmin(): void + { + if (!$this->hasGroupAdminRole()) { + throw new AccessDeniedHttpException('The group admin role is required to access this resource.'); + } + } +} diff --git a/src/Security/Checker/UserEmailConfirmedChecker.php b/src/Security/Checker/UserEmailConfirmedChecker.php new file mode 100644 index 0000000..ec25828 --- /dev/null +++ b/src/Security/Checker/UserEmailConfirmedChecker.php @@ -0,0 +1,30 @@ + 10])] +class UserEmailConfirmedChecker implements UserCheckerInterface +{ + public function checkPreAuth(UserInterface $user): void + { + /** @var User $user */ + if (!$user->isEmailConfirmed()) { + throw new AccountEmailNotConfirmedException(); + } + } + + public function checkPostAuth(UserInterface $user): void + { + } +} diff --git a/src/Security/Checker/UserEnabledChecker.php b/src/Security/Checker/UserEnabledChecker.php new file mode 100644 index 0000000..5c9c78b --- /dev/null +++ b/src/Security/Checker/UserEnabledChecker.php @@ -0,0 +1,30 @@ + 10])] +class UserEnabledChecker implements UserCheckerInterface +{ + public function checkPreAuth(UserInterface $user): void + { + /** @var User $user */ + if (!$user->isEnabled()) { + throw new AccountDisabledException(); + } + } + + public function checkPostAuth(UserInterface $user): void + { + } +} diff --git a/src/Security/EntryPoint/AuthenticationEntryPoint.php b/src/Security/EntryPoint/AuthenticationEntryPoint.php new file mode 100644 index 0000000..644010c --- /dev/null +++ b/src/Security/EntryPoint/AuthenticationEntryPoint.php @@ -0,0 +1,40 @@ +attributes->get('_route'); + if ($authException instanceof InsufficientAuthenticationException && u($route)->startsWith('_api_')) { + throw new UnauthorizedHttpException('', $authException->getMessage(), $authException); + } + + return new RedirectResponse($this->urlGenerator->generate('app_login')); + } +} diff --git a/src/Security/Exception/AccountDisabledException.php b/src/Security/Exception/AccountDisabledException.php new file mode 100644 index 0000000..7e357a6 --- /dev/null +++ b/src/Security/Exception/AccountDisabledException.php @@ -0,0 +1,20 @@ +getUser(); + + // the user must be logged in; if not, deny access + if (!$user instanceof User) { + return false; + } + + /** @var Product $subject */ + + return match ($attribute) { + self::EDIT => $this->canEdit($subject, $user), + self::DUPLICATE => $this->canDuplicate($subject, $user), + self::BORROW => $this->canBorrow($subject, $user), + self::DELETE => $this->canDelete($subject, $user), + default => throw new \LogicException('This code should not be reached!') + }; + } + + private function canEdit(Product $product, User $user): bool + { + return $product->isOwner($user); + } + + private function canDuplicate(Product $product, User $user): bool + { + return $product->isOwner($user); + } + + private function canBorrow(Product $product, User $user): bool + { + // 1. we can't borrow or own products + if ($user === $product->getOwner()) { + return false; + } + + return true; + } + + private function canDelete(Product $product, User $user): bool + { + return $product->isOwner($user) && !$product->hasOngoingServiceRequests(); + } +} diff --git a/src/Security/Voter/ServiceRequest/ServiceRequestVoter.php b/src/Security/Voter/ServiceRequest/ServiceRequestVoter.php new file mode 100644 index 0000000..c4709fa --- /dev/null +++ b/src/Security/Voter/ServiceRequest/ServiceRequestVoter.php @@ -0,0 +1,53 @@ +getUser(); + + // the user must be logged in; if not, deny access + if (!$user instanceof User) { + return false; + } + + /** @var ServiceRequest $subject */ + + return match ($attribute) { + self::VIEW => $this->canView($subject, $user), + default => throw new \LogicException('This code should not be reached!') + }; + } + + /** + * A user can view the conversation of a service request if he is the owner + * or the recipient of the service. + */ + private function canView(ServiceRequest $serviceRequest, User $user): bool + { + return $serviceRequest->isOwnerOrRecipient($user); + } +} diff --git a/src/Serializer/ProductDocumentNormalizer.php b/src/Serializer/ProductDocumentNormalizer.php new file mode 100644 index 0000000..e34b23b --- /dev/null +++ b/src/Serializer/ProductDocumentNormalizer.php @@ -0,0 +1,53 @@ + $context + * + * @return array + * + * @throws ExceptionInterface + */ + public function normalize(mixed $object, string $format = null, array $context = []): array + { + /** @var array $data */ + $data = $this->normalizer->normalize($object, $format, $context); + + // Meilisearch doesn't support null values for the _geo field for now + // @see https://github.com/meilisearch/meilisearch/issues/3497 + // fixed in 1.1 to test without this fix + if (\array_key_exists('_geo', $data) && $data['_geo'] === null) { + unset($data['_geo']); + } + + return $data; + } + + /** + * @param array $context + */ + public function supportsNormalization($data, string $format = null, array $context = []): bool + { + return $data instanceof ProductDocument; + } +} diff --git a/src/State/GroupGetStatsProvider.php b/src/State/GroupGetStatsProvider.php new file mode 100644 index 0000000..3a9a372 --- /dev/null +++ b/src/State/GroupGetStatsProvider.php @@ -0,0 +1,29 @@ + + */ +final class GroupGetStatsProvider implements ProviderInterface +{ + public function __construct( + readonly private GroupRepository $groupRepository + ) { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null // @phpstan-ignore-line + { + $groupStats = new GroupResource(); + $groupStats->count = $this->groupRepository->count([]); + + return $groupStats; + } +} diff --git a/src/State/Processor/ProductSwitchProcessor.php b/src/State/Processor/ProductSwitchProcessor.php new file mode 100644 index 0000000..86e66bb --- /dev/null +++ b/src/State/Processor/ProductSwitchProcessor.php @@ -0,0 +1,35 @@ + $uriVariables + * @param array $context + */ + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Product + { + Assert::isInstanceOf($data, Product::class); + $data->switchStatus(); + $this->productRepository->save($data, true); + + return $data; + } +} diff --git a/src/Subscriber/Security/CrudControllerSubscriber.php b/src/Subscriber/Security/CrudControllerSubscriber.php new file mode 100644 index 0000000..17a2a2e --- /dev/null +++ b/src/Subscriber/Security/CrudControllerSubscriber.php @@ -0,0 +1,43 @@ + 'onController', + ]; + } + + public function onController(ControllerEvent $event): void + { + $controller = $event->getController(); + if (\is_array($controller)) { + $ctrl = $controller[0] ?? null; + if (is_a($ctrl, AdminSecuredCrudControllerInterface::class)) { + $this->authorizationChecker->checkAdminRole(); + } + if (is_a($ctrl, GroupAdminSecuredCrudControllerInterface::class)) { + $this->authorizationChecker->isGroupAdmin(); + } + } + } +} diff --git a/src/Subscriber/SecuritySubscriber.php b/src/Subscriber/SecuritySubscriber.php new file mode 100644 index 0000000..49d0eb8 --- /dev/null +++ b/src/Subscriber/SecuritySubscriber.php @@ -0,0 +1,41 @@ + 'onLoginSuccess', + ]; + } + + public function onLoginSuccess(LoginSuccessEvent $event): void + { + /** @var User $user */ + $user = $event->getUser(); + + // redirect admins to the admin space + if ($user->isAdmin()) { + $event->setResponse(new RedirectResponse($this->router->generate('admin', [], UrlGeneratorInterface::ABSOLUTE_URL))); + } + + $this->userManager->updateLoginAt($user); + } +} diff --git a/src/Subscriber/Workflow/Guard/ServiceRequestAcceptTransitionSubscriber.php b/src/Subscriber/Workflow/Guard/ServiceRequestAcceptTransitionSubscriber.php new file mode 100644 index 0000000..39e5cf8 --- /dev/null +++ b/src/Subscriber/Workflow/Guard/ServiceRequestAcceptTransitionSubscriber.php @@ -0,0 +1,44 @@ +setBlocked(false); + + /** @var ServiceRequest $sr */ + $sr = $event->getSubject(); + + /** @var ?User $user */ + $user = $this->security->getUser(); + if ($user !== null && !$sr->isOwner($user)) { + $event->setBlocked(true, 'Only the owner of the object can trigger the "accept" transition.'); + } + } + + public static function getSubscribedEvents(): array + { + return [ + ServiceRequestStatusWorkflow::WORKFLOW_SERVICE_REQUEST_STATUS_GUARD_ACCEPT_EVENT => ['guardAccept'], + ]; + } +} diff --git a/src/Subscriber/Workflow/Guard/ServiceRequestAutoFinalizeTransitionSubscriber.php b/src/Subscriber/Workflow/Guard/ServiceRequestAutoFinalizeTransitionSubscriber.php new file mode 100644 index 0000000..a68892d --- /dev/null +++ b/src/Subscriber/Workflow/Guard/ServiceRequestAutoFinalizeTransitionSubscriber.php @@ -0,0 +1,41 @@ +setBlocked(false); + + /** @var ServiceRequest $sr */ + $sr = $event->getSubject(); + $today = new \DateTimeImmutable('today'); + if ($today <= $sr->getEndAt()) { + $event->setBlocked(true, 'the autoFinalize is blocked if the endAt is before or today.'); + } + } + + public static function getSubscribedEvents(): array + { + return [ + ServiceRequestStatusWorkflow::WORKFLOW_SERVICE_REQUEST_STATUS_GUARD_AUTO_FINALIZE_EVENT => ['guardAutoFinalize'], + ]; + } +} diff --git a/src/Subscriber/Workflow/Guard/ServiceRequestConfirmTransitionSubscriber.php b/src/Subscriber/Workflow/Guard/ServiceRequestConfirmTransitionSubscriber.php new file mode 100644 index 0000000..063dc6a --- /dev/null +++ b/src/Subscriber/Workflow/Guard/ServiceRequestConfirmTransitionSubscriber.php @@ -0,0 +1,44 @@ +setBlocked(false); + + /** @var ServiceRequest $serviceRequest */ + $serviceRequest = $event->getSubject(); + + /** @var ?User $user */ + $user = $this->security->getUser(); + if ($user !== null && !$serviceRequest->isRecipient($user)) { + $event->setBlocked(true, 'Only the recipient of the object can trigger the "confirm" transition.'); + } + } + + public static function getSubscribedEvents(): array + { + return [ + ServiceRequestStatusWorkflow::WORKFLOW_SERVICE_REQUEST_STATUS_GUARD_CONFIRM_EVENT => ['guardConfirm'], + ]; + } +} diff --git a/src/Subscriber/Workflow/Guard/ServiceRequestFinalizeTransitionSubscriber.php b/src/Subscriber/Workflow/Guard/ServiceRequestFinalizeTransitionSubscriber.php new file mode 100644 index 0000000..851b6b2 --- /dev/null +++ b/src/Subscriber/Workflow/Guard/ServiceRequestFinalizeTransitionSubscriber.php @@ -0,0 +1,49 @@ +setBlocked(false); + + /** @var ServiceRequest $sr */ + $sr = $event->getSubject(); + + /** @var ?User $user */ + $user = $this->security->getUser(); + if ($user !== null && !$sr->isOwner($user)) { + $event->setBlocked(true, 'Only the owner of the object can trigger the "finalize" transition.'); + } + + $today = new \DateTimeImmutable('today'); + if ($sr->getStartAt() > $today) { + $event->setBlocked(true, 'We must be in the transaction interval to finalize it.'); + } + } + + public static function getSubscribedEvents(): array + { + return [ + ServiceRequestStatusWorkflow::WORKFLOW_SERVICE_REQUEST_STATUS_GUARD_FINALIZE_EVENT => ['guardFinalize'], + ]; + } +} diff --git a/src/Subscriber/Workflow/Guard/ServiceRequestModifyOwnerTransitionSubscriber.php b/src/Subscriber/Workflow/Guard/ServiceRequestModifyOwnerTransitionSubscriber.php new file mode 100644 index 0000000..c1f7d36 --- /dev/null +++ b/src/Subscriber/Workflow/Guard/ServiceRequestModifyOwnerTransitionSubscriber.php @@ -0,0 +1,44 @@ +setBlocked(false); + + /** @var ServiceRequest $sr */ + $sr = $event->getSubject(); + + /** @var ?User $user */ + $user = $this->security->getUser(); + if ($user !== null && !$sr->isOwner($user)) { + $event->setBlocked(true, 'Only the owner of the object can trigger the "modifyOwner" transition.'); + } + } + + public static function getSubscribedEvents(): array + { + return [ + ServiceRequestStatusWorkflow::WORKFLOW_SERVICE_REQUEST_STATUS_GUARD_MODIFY_OWNER_EVENT => ['guardModifyOwner'], + ]; + } +} diff --git a/src/Subscriber/Workflow/Guard/ServiceRequestModifyRecipientTransitionSubscriber.php b/src/Subscriber/Workflow/Guard/ServiceRequestModifyRecipientTransitionSubscriber.php new file mode 100644 index 0000000..ff6b076 --- /dev/null +++ b/src/Subscriber/Workflow/Guard/ServiceRequestModifyRecipientTransitionSubscriber.php @@ -0,0 +1,44 @@ +setBlocked(false); + + /** @var ServiceRequest $serviceRequest */ + $serviceRequest = $event->getSubject(); + + /** @var ?User $user */ + $user = $this->security->getUser(); + if ($user !== null && !$serviceRequest->isRecipient($user)) { + $event->setBlocked(true, 'Only the recipient of the object can trigger the "modifyRecipient" transition.'); + } + } + + public static function getSubscribedEvents(): array + { + return [ + ServiceRequestStatusWorkflow::WORKFLOW_SERVICE_REQUEST_STATUS_GUARD_MODIFY_RECIPIENT_EVENT => ['guardModifyRecipient'], + ]; + } +} diff --git a/src/Subscriber/Workflow/ServiceRequestAcceptedSubscriber.php b/src/Subscriber/Workflow/ServiceRequestAcceptedSubscriber.php new file mode 100644 index 0000000..2708df5 --- /dev/null +++ b/src/Subscriber/Workflow/ServiceRequestAcceptedSubscriber.php @@ -0,0 +1,61 @@ + 'onCompleted', + ]; + } + + public function onCompleted(Event $event): void + { + /** @var ServiceRequest $serviceRequest */ + $serviceRequest = $event->getSubject(); + $this->createSystemMessage($serviceRequest); + $this->appMailer->send(ServiceRequestAccepted::class, ['service_request' => $serviceRequest]); + $this->sendSms($serviceRequest->getRecipient(), ServiceRequestAccepted::class); + } + + private function createSystemMessage(ServiceRequest $serviceRequest): void + { + $product = $serviceRequest->getProduct(); + $systemMessage = $this->messageManager->createSystemMessage( + $serviceRequest, + self::MESSAGE_SYSTEM_ACCEPTED.'.'.$product->getType()->value, + ); + $this->messageManager->save($systemMessage, true); + } +} diff --git a/src/Subscriber/Workflow/ServiceRequestConfirmedSubscriber.php b/src/Subscriber/Workflow/ServiceRequestConfirmedSubscriber.php new file mode 100644 index 0000000..59bda5d --- /dev/null +++ b/src/Subscriber/Workflow/ServiceRequestConfirmedSubscriber.php @@ -0,0 +1,59 @@ + 'onCompleted', + ]; + } + + public function onCompleted(Event $event): void + { + /** @var ServiceRequest $serviceRequest */ + $serviceRequest = $event->getSubject(); + $this->createSystemMessage($serviceRequest); + $this->appMailer->send(ServiceRequestConfirmed::class, ['service_request' => $serviceRequest]); + $this->sendSms($serviceRequest->getOwner(), ServiceRequestConfirmed::class); + } + + private function createSystemMessage(ServiceRequest $serviceRequest): void + { + $product = $serviceRequest->getProduct(); + $systemMessage = $this->messageManager->createSystemMessage( + $serviceRequest, + self::MESSAGE_SYSTEM_CONFIRMED.'.'.$product->getType()->value, + ); + $this->messageManager->save($systemMessage, true); + } +} diff --git a/src/Subscriber/Workflow/ServiceRequestFinishedSubscriber.php b/src/Subscriber/Workflow/ServiceRequestFinishedSubscriber.php new file mode 100644 index 0000000..0128775 --- /dev/null +++ b/src/Subscriber/Workflow/ServiceRequestFinishedSubscriber.php @@ -0,0 +1,66 @@ + 'onCompleted', + ServiceRequestStatusWorkflow::WORKFLOW_SERVICE_REQUEST_COMPLETED_AUTO_FINALIZE_EVENT => 'onCompletedAuto', + ]; + } + + /** + * For a manual finalization, we set the end to the current date. + */ + public function onCompleted(Event $event): void + { + /** @var ServiceRequest $serviceRequest */ + $serviceRequest = $event->getSubject(); + $serviceRequest->setEndAt(new \DateTimeImmutable('today')); + $this->serviceRequestManager->deleteUnavailabilities($serviceRequest); + $this->serviceRequestManager->save($serviceRequest, true); + $this->createSystemMessage($serviceRequest); + } + + /** + * For an auto finalize, the end date doesn't change, but we set the auto message + * date to the day after the end date. + */ + public function onCompletedAuto(Event $event): void + { + /** @var ServiceRequest $serviceRequest */ + $serviceRequest = $event->getSubject(); + $this->serviceRequestManager->deleteUnavailabilities($serviceRequest); + $this->createSystemMessage($serviceRequest, $serviceRequest->getFinalizedAt()); + } + + private function createSystemMessage(ServiceRequest $serviceRequest, \DateTimeImmutable $createdAt = null): void + { + $systemMessage = $this->messageManager->createSystemMessage( + $serviceRequest, + self::MESSAGE_SYSTEM_FINALIZED, + createdAt: $createdAt + ); + $this->messageManager->save($systemMessage, true); + } +} diff --git a/src/Subscriber/Workflow/ServiceRequestModifiedByOwnerSubscriber.php b/src/Subscriber/Workflow/ServiceRequestModifiedByOwnerSubscriber.php new file mode 100644 index 0000000..855562c --- /dev/null +++ b/src/Subscriber/Workflow/ServiceRequestModifiedByOwnerSubscriber.php @@ -0,0 +1,67 @@ + 'onCompleted', + ]; + } + + public function onCompleted(Event $event): void + { + /** @var ServiceRequest $serviceRequest */ + $serviceRequest = $event->getSubject(); + $this->createSystemMessage($serviceRequest); + $this->appMailer->send(ServiceRequestModifiedByOwner::class, ['service_request' => $serviceRequest]); + $this->sendSms($serviceRequest->getRecipient(), ServiceRequestModifiedByOwner::class); + } + + private function createSystemMessage(ServiceRequest $serviceRequest): void + { + $product = $serviceRequest->getProduct(); + $dateFormat = $this->translator->trans('format.date', [], 'date'); + $systemMessage = $this->messageManager->createSystemMessage( + $serviceRequest, + self::MESSAGE_SYSTEM_MODIFIED_BY_OWNER.'.'.$product->getType()->value, + [ + '%startAt%' => $serviceRequest->getStartAt()->format($dateFormat), + '%endAt%' => $serviceRequest->getEndAt()->format($dateFormat), + ] + ); + $this->messageManager->save($systemMessage, true); + } +} diff --git a/src/Subscriber/Workflow/ServiceRequestModifiedByRecipientSubscriber.php b/src/Subscriber/Workflow/ServiceRequestModifiedByRecipientSubscriber.php new file mode 100644 index 0000000..c010b7c --- /dev/null +++ b/src/Subscriber/Workflow/ServiceRequestModifiedByRecipientSubscriber.php @@ -0,0 +1,66 @@ + 'onCompleted', + ]; + } + + public function onCompleted(Event $event): void + { + /** @var ServiceRequest $serviceRequest */ + $serviceRequest = $event->getSubject(); + $this->createSystemMessage($serviceRequest); + $this->appMailer->send(ServiceRequestModifiedByRecipient::class, ['service_request' => $serviceRequest]); + $this->sendSms($serviceRequest->getOwner(), ServiceRequestModifiedByRecipient::class); + } + + private function createSystemMessage(ServiceRequest $serviceRequest): void + { + $product = $serviceRequest->getProduct(); + $dateFormat = $this->translator->trans('format.date', [], 'date'); + $systemMessage = $this->messageManager->createSystemMessage( + $serviceRequest, + self::MESSAGE_SYSTEM_MODIFIED_BY_RECIPIENT.'.'.$product->getType()->value, + [ + '%startAt%' => $serviceRequest->getStartAt()->format($dateFormat), + '%endAt%' => $serviceRequest->getEndAt()->format($dateFormat), + ] + ); + $this->messageManager->save($systemMessage, true); + } +} diff --git a/src/Subscriber/Workflow/ServiceRequestRefusedSubscriber.php b/src/Subscriber/Workflow/ServiceRequestRefusedSubscriber.php new file mode 100644 index 0000000..0c195ca --- /dev/null +++ b/src/Subscriber/Workflow/ServiceRequestRefusedSubscriber.php @@ -0,0 +1,76 @@ + 'onCompleted', + ]; + } + + public function onCompleted(Event $event): void + { + /** @var ServiceRequest $serviceRequest */ + $serviceRequest = $event->getSubject(); + /** @var ?User $actor */ + $actor = $this->security->getUser(); + $this->createSystemMessage($serviceRequest, $actor); + $this->sendEmail($serviceRequest, $actor); + $this->sendSms($serviceRequest->getOtherUser($actor), ServiceRequestRefused::class); + $this->serviceRequestManager->deleteUnavailabilities($serviceRequest); + } + + private function createSystemMessage(ServiceRequest $serviceRequest, ?User $actor): void + { + $product = $serviceRequest->getProduct(); + $systemMessage = $this->messageManager->createSystemMessage( + $serviceRequest, + self::MESSAGE_SYSTEM_REFUSED.'.'.$product->getType()->value, + ['%actor%' => $actor?->getDisplayName() ?? ''], + ); + $this->messageManager->save($systemMessage, true); + } + + private function sendEmail(ServiceRequest $serviceRequest, ?User $actor): void + { + $this->appMailer->send(ServiceRequestRefused::class, [ + 'service_request' => $serviceRequest, + 'actor' => $actor, + ]); + } +} diff --git a/src/Test/ContainerRepositoryTrait.php b/src/Test/ContainerRepositoryTrait.php new file mode 100755 index 0000000..a718071 --- /dev/null +++ b/src/Test/ContainerRepositoryTrait.php @@ -0,0 +1,116 @@ +get('doctrine')->getManager()->clear(); + } + + public function getAddressRepository(): AddressRepository + { + return self::getContainer()->get(AddressRepository::class); + } + + public function getCategoryRepository(): CategoryRepository + { + return self::getContainer()->get(CategoryRepository::class); + } + + public function getGroupRepository(): GroupRepository + { + return self::getContainer()->get(GroupRepository::class); + } + + public function getUserRepository(): UserRepository + { + return self::getContainer()->get(UserRepository::class); + } + + public function getUserGroupRepository(): UserGroupRepository + { + return self::getContainer()->get(UserGroupRepository::class); + } + + public function getGroupOfferRepository(): GroupOfferRepository + { + return self::getContainer()->get(GroupOfferRepository::class); + } + + public function getUserManager(): UserManager + { + return self::getContainer()->get(UserManager::class); + } + + public function getProductManager(): ProductManager + { + return self::getContainer()->get(ProductManager::class); + } + + public function getConfigurationRepository(): ConfigurationRepository + { + return self::getContainer()->get(ConfigurationRepository::class); + } + + public function getProductRepository(): ProductRepository + { + return self::getContainer()->get(ProductRepository::class); + } + + public function getPaymentRepository(): PaymentRepository + { + return self::getContainer()->get(PaymentRepository::class); + } + + public function getProductAvailabilityRepository(): ProductAvailabilityRepository + { + return self::getContainer()->get(ProductAvailabilityRepository::class); + } + + public function getMenuRepository(): MenuRepository + { + return self::getContainer()->get(MenuRepository::class); + } + + public function getMenuItemRepository(): MenuItemRepository + { + return self::getContainer()->get(MenuItemRepository::class); + } + + public function getMessageRepository(): MessageRepository + { + return self::getContainer()->get(MessageRepository::class); + } + + public function getServiceRequestRepository(): ServiceRequestRepository + { + return self::getContainer()->get(ServiceRequestRepository::class); + } +} diff --git a/src/Test/ContainerTrait.php b/src/Test/ContainerTrait.php new file mode 100644 index 0000000..b9898b8 --- /dev/null +++ b/src/Test/ContainerTrait.php @@ -0,0 +1,68 @@ +get('doctrine')->getManager()->clear(); + } + + public function getValidationProcessor(): ValidationProcessor + { + return self::getContainer()->get(ValidationProcessor::class); + } + + public function getCategoryExtension(): CategoryExtension + { + return self::getContainer()->get(CategoryExtension::class); + } + + public function getUserExtension(): UserExtension + { + return self::getContainer()->get(UserExtension::class); + } + + public function getI18nExtension(): i18nExtension + { + return self::getContainer()->get(i18nExtension::class); + } + + public function getTwigExtension(): TwigExtension + { + return self::getContainer()->get(TwigExtension::class); + } + + public function getMenuExtension(): MenuExtension + { + return self::getContainer()->get(MenuExtension::class); + } + + public function getFlysystemExtension(): FlysystemExtension + { + return self::getContainer()->get(FlysystemExtension::class); + } + + public function getEntityExtension(): EntityExtension + { + return self::getContainer()->get(EntityExtension::class); + } +} diff --git a/src/Test/KernelTrait.php b/src/Test/KernelTrait.php new file mode 100644 index 0000000..a6ddf99 --- /dev/null +++ b/src/Test/KernelTrait.php @@ -0,0 +1,80 @@ +getContainer(); + $this->fixDoctrineBug($container); + + /** @var UserRepository $userRepository */ + $userRepository = self::getContainer()->get(UserRepository::class); + $user = $userRepository->get($id); + $client->loginUser($user); + } + + public function logout(KernelBrowser $client): void + { + $client->request('GET', '/logout'); + self::assertResponseRedirects(); + $client->followRedirect(); + } + + public function loginAsAdmin(KernelBrowser|Client $client): void + { + $this->login($client, TestReference::ADMIN_LOIC); + } + + public function loginAsSarah(KernelBrowser|Client $client): void + { + $this->login($client, TestReference::ADMIN_SARAH); + } + + public function loginAsKevin(KernelBrowser|Client $client): void + { + $this->login($client, TestReference::ADMIN_KEVIN); + } + + public function loginAsUser(KernelBrowser|Client $client): void + { + $this->login($client, TestReference::USER_17); + } + + /** + * - No address associated yet. + * - Is group admin of Group 1. + */ + public function loginAsUser16(KernelBrowser|Client $client): void + { + $this->login($client, TestReference::USER_16); + } + + /** + * - Has a pending invitation for the group 1 group. + */ + public function loginAsUser11(KernelBrowser|Client $client): void + { + $this->login($client, TestReference::USER_11); + } + + /** + * - Has a published product on the Apes place. + */ + public function loginAsPlaceApes(KernelBrowser|Client $client): void + { + $this->login($client, TestReference::PLACE_APES); + } +} diff --git a/src/Translator/NoTranslator.php b/src/Translator/NoTranslator.php new file mode 100644 index 0000000..08a007a --- /dev/null +++ b/src/Translator/NoTranslator.php @@ -0,0 +1,63 @@ + $parameters + */ + public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null): string + { + // to find EasyAdmin translations codes, uncomment this +// if ($domain === 'EasyAdminBundle') { +// dump($id); +// } + + return $id; + } + + public function getCatalogues(): array + { + return $this->translator->getCatalogues(); + } + + public function getCatalogue(string $locale = null): MessageCatalogueInterface + { + return $this->translator->getCatalogue($locale); + } + + public function setLocale(string $locale): void + { + $this->translator->setLocale($locale); + } + + public function getLocale(): string + { + return $this->translator->getLocale(); + } +} diff --git a/src/Twig/CategoryExtension.php b/src/Twig/CategoryExtension.php new file mode 100755 index 0000000..f498ad6 --- /dev/null +++ b/src/Twig/CategoryExtension.php @@ -0,0 +1,33 @@ +categoryStorage->publicUrl((string) $category->getImage()); + } +} diff --git a/src/Twig/Extension/EntityExtension.php b/src/Twig/Extension/EntityExtension.php new file mode 100644 index 0000000..951926b --- /dev/null +++ b/src/Twig/Extension/EntityExtension.php @@ -0,0 +1,34 @@ + + */ + public function getFunctions(): array + { + return [ + new TwigFunction('is_image_entity', $this->isImageEntity(...)), + new TwigFunction('is_images_entity', $this->isImagesEntity(...)), + ]; + } + + public function isImageEntity(object $entity): bool + { + return $entity instanceof ImageInterface; + } + + public function isImagesEntity(object $entity): bool + { + return $entity instanceof ImagesInterface; + } +} diff --git a/src/Twig/Extension/ResponseExtension.php b/src/Twig/Extension/ResponseExtension.php new file mode 100644 index 0000000..33187b8 --- /dev/null +++ b/src/Twig/Extension/ResponseExtension.php @@ -0,0 +1,24 @@ +getStatusText(...)), + ]; + } + + public function getStatusText(int $errorCode): string + { + return Response::$statusTexts[$errorCode] ?? 'Unknown error code'; + } +} diff --git a/src/Twig/Extension/i18nExtension.php b/src/Twig/Extension/i18nExtension.php new file mode 100644 index 0000000..d5ae355 --- /dev/null +++ b/src/Twig/Extension/i18nExtension.php @@ -0,0 +1,36 @@ +getI18Prefix(...)), + ]; + } + + /** + * Convert a Twig template name to a i18n prefix to use in XLIFF files. + */ + public function getI18Prefix(string $temlateName): string + { + $temlateName = u($temlateName)->trimSuffix('.html.twig'); + $hierarchy = u($temlateName->toString())->split('/'); + + // apply snake case on each entry (which also applis lower) + $hierarchy = array_map(static fn (UnicodeString $string) => $string->snake()->toString(), $hierarchy); + + // then join the folders with a dot with the templates prefix + return 'templates.'.implode('.', $hierarchy); + } +} diff --git a/src/Twig/FlysystemExtension.php b/src/Twig/FlysystemExtension.php new file mode 100644 index 0000000..d949f96 --- /dev/null +++ b/src/Twig/FlysystemExtension.php @@ -0,0 +1,58 @@ + + */ + public function getFilters(): array + { + return [ + new TwigFilter('public_url', $this->getPublicUrl(...)), + new TwigFilter('public_url_image', $this->getPublicUrlImage(...)), + ]; + } + + /** + * Loop through all extensions implementing the Flysystem publicUrl() function. + */ + public function getPublicUrl(ImageInterface $entity): ?string + { + foreach ($this->imageExtensionCollection->getExtensions() as $extension) { + if ($extension->supports($entity)) { + return $extension->getPublicUrl($entity); + } + } + + throw new \LogicException('This entity is not managed by this function, add the case.'); + } + + /** + * Same as getPublicUrl() but for entities having multiple images associated. + */ + public function getPublicUrlImage(ImagesInterface $entity, string $image): ?string + { + foreach ($this->imagesExtensionCollection->getExtensions() as $extension) { + if ($extension->supports($entity)) { + return $extension->getPublicUrl($image); + } + } + + throw new \LogicException('This entity is not managed by this function, add the case.'); + } +} diff --git a/src/Twig/FlysystemImageInterface.php b/src/Twig/FlysystemImageInterface.php new file mode 100644 index 0000000..0847202 --- /dev/null +++ b/src/Twig/FlysystemImageInterface.php @@ -0,0 +1,14 @@ + $extensions + */ + public function __construct( + private readonly iterable $extensions, + ) { + } + + /** + * @return iterable + */ + public function getExtensions(): iterable + { + return $this->extensions; + } +} diff --git a/src/Twig/ImagesExtensionCollection.php b/src/Twig/ImagesExtensionCollection.php new file mode 100644 index 0000000..e36e787 --- /dev/null +++ b/src/Twig/ImagesExtensionCollection.php @@ -0,0 +1,27 @@ + $extensions + */ + public function __construct( + private readonly iterable $extensions, + ) { + } + + /** + * @return iterable + */ + public function getExtensions(): iterable + { + return $this->extensions; + } +} diff --git a/src/Twig/MenuExtension.php b/src/Twig/MenuExtension.php new file mode 100644 index 0000000..5034441 --- /dev/null +++ b/src/Twig/MenuExtension.php @@ -0,0 +1,33 @@ +defaultStorage->publicUrl((string) $menu->getImage()); + } +} diff --git a/src/Twig/ProductExtension.php b/src/Twig/ProductExtension.php new file mode 100755 index 0000000..1117475 --- /dev/null +++ b/src/Twig/ProductExtension.php @@ -0,0 +1,28 @@ +productStorage->publicUrl($image); + } +} diff --git a/src/Twig/TwigExtension.php b/src/Twig/TwigExtension.php new file mode 100644 index 0000000..e5bca16 --- /dev/null +++ b/src/Twig/TwigExtension.php @@ -0,0 +1,34 @@ + + */ + public function getFilters(): array + { + return [ + new TwigFilter('snake', $this->snake(...)), + ]; + } + + public function snake(?string $sring): string + { + return u($sring)->snake()->toString(); + } +} diff --git a/src/Twig/UserExtension.php b/src/Twig/UserExtension.php new file mode 100644 index 0000000..0cd70b5 --- /dev/null +++ b/src/Twig/UserExtension.php @@ -0,0 +1,33 @@ +userStorage->publicUrl((string) $user->getImage()); + } +} diff --git a/src/Validator/Constraints/Category/CategoryParentNotSelf.php b/src/Validator/Constraints/Category/CategoryParentNotSelf.php new file mode 100644 index 0000000..0c177a2 --- /dev/null +++ b/src/Validator/Constraints/Category/CategoryParentNotSelf.php @@ -0,0 +1,18 @@ +hasForbiddenParent($value, $value->getParent())) { + $this->context->buildViolation($constraint->message) + ->atPath('parent') + ->addViolation(); + } + } + + /** + * Check recursivly that the parent or grand parent is not equal to the current + * category. + */ + private function hasForbiddenParent(Category $group, ?Category $parent): bool + { + if ($group === $parent) { + return true; + } + + if ($parent === null) { + return false; + } + + return $this->hasForbiddenParent($group, $parent->getParent()); + } +} diff --git a/src/Validator/Constraints/File.php b/src/Validator/Constraints/File.php new file mode 100755 index 0000000..d4a267c --- /dev/null +++ b/src/Validator/Constraints/File.php @@ -0,0 +1,18 @@ +hasForbiddenParent($value, $value->getParent())) { + $this->context->buildViolation($constraint->message) + ->atPath('parent') + ->addViolation(); + } + } + + /** + * Check recursivly that the parent or grand parent is not equal to the current + * group. + */ + private function hasForbiddenParent(Group $group, ?Group $parent): bool + { + if ($group === $parent) { + return true; + } + + if ($parent === null) { + return false; + } + + return $this->hasForbiddenParent($group, $parent->getParent()); + } +} diff --git a/src/Validator/Constraints/MenuItem/MenuItemParentNotSelf.php b/src/Validator/Constraints/MenuItem/MenuItemParentNotSelf.php new file mode 100644 index 0000000..c3d0f2b --- /dev/null +++ b/src/Validator/Constraints/MenuItem/MenuItemParentNotSelf.php @@ -0,0 +1,18 @@ +hasForbiddenParent($value, $value->getParent())) { + $this->context->buildViolation($constraint->message) + ->atPath('parent') + ->addViolation(); + } + } + + /** + * Check recursivly that the parent or grand parent is not equal to the current + * menu item. + */ + private function hasForbiddenParent(MenuItem $menuItem, ?MenuItem $parent): bool + { + if ($menuItem === $parent) { + return true; + } + + if ($parent === null) { + return false; + } + + return $this->hasForbiddenParent($menuItem, $parent->getParent()); + } +} diff --git a/src/Validator/Constraints/ServiceRequest/ProductAvailabilityNoOverlap.php b/src/Validator/Constraints/ServiceRequest/ProductAvailabilityNoOverlap.php new file mode 100644 index 0000000..68c6ff6 --- /dev/null +++ b/src/Validator/Constraints/ServiceRequest/ProductAvailabilityNoOverlap.php @@ -0,0 +1,18 @@ +getProduct()->getUnavailabilities($value); + + // compute effective dates for the new service request + $srPeriod = CarbonInterval::days(1)->toPeriod($value->getStartAt(), $value->getEndAt()); + $srDays = array_map(static fn (CarbonInterface $date) => $date->format('Y-m-d'), $srPeriod->toArray()); + + // error if there is at least one day that overlap + if (\count(array_intersect($unavailabilities, $srDays)) > 0) { + $this->context->buildViolation($constraint->message) + ->atPath('startAt') + ->addViolation(); + } + } +} diff --git a/src/Workflow/ServiceRequestStatusWorkflow.php b/src/Workflow/ServiceRequestStatusWorkflow.php new file mode 100644 index 0000000..b995aeb --- /dev/null +++ b/src/Workflow/ServiceRequestStatusWorkflow.php @@ -0,0 +1,183 @@ +accept($sr); + break; + case ServiceRequestStatusTransition::CONFIRM: + $this->confirm($sr); + break; + case ServiceRequestStatusTransition::MODIFY_OWNER: + $this->modifyOwner($sr); + break; + case ServiceRequestStatusTransition::MODIFY_RECIPIENT: + $this->modifyRecipient($sr); + break; + case ServiceRequestStatusTransition::REFUSE: + $this->refuse($sr); + break; + case ServiceRequestStatusTransition::FINALIZE: + $this->finalize($sr); + break; + } + } + + private function getException(ServiceRequest $sr, ServiceRequestStatusTransition $transition): \LogicException + { + return new \LogicException(sprintf(self::EXCEPTION_MESSAGE, $transition->name, $sr->getId(), $sr->getStatus()->value)); + } + + public function canAccept(ServiceRequest $sr): bool + { + return $this->serviceRequestStatusStateMachine->can($sr, ServiceRequestStatusTransition::ACCEPT->value); + } + + public function accept(ServiceRequest $sr): ServiceRequest + { + if (!$this->canAccept($sr)) { + throw $this->getException($sr, ServiceRequestStatusTransition::ACCEPT); + } + + $this->serviceRequestStatusStateMachine->apply($sr, ServiceRequestStatusTransition::ACCEPT->value); + + return $sr; + } + + public function canModifyOwner(ServiceRequest $sr): bool + { + return $this->serviceRequestStatusStateMachine->can($sr, ServiceRequestStatusTransition::MODIFY_OWNER->value); + } + + public function modifyOwner(ServiceRequest $sr): ServiceRequest + { + if (!$this->canModifyOwner($sr)) { + throw $this->getException($sr, ServiceRequestStatusTransition::MODIFY_OWNER); + } + + $this->serviceRequestStatusStateMachine->apply($sr, ServiceRequestStatusTransition::MODIFY_OWNER->value); + + return $sr; + } + + public function canModifyRecipient(ServiceRequest $sr): bool + { + return $this->serviceRequestStatusStateMachine->can($sr, ServiceRequestStatusTransition::MODIFY_RECIPIENT->value); + } + + public function modifyRecipient(ServiceRequest $sr): ServiceRequest + { + if (!$this->canModifyRecipient($sr)) { + throw $this->getException($sr, ServiceRequestStatusTransition::MODIFY_RECIPIENT); + } + + $this->serviceRequestStatusStateMachine->apply($sr, ServiceRequestStatusTransition::MODIFY_RECIPIENT->value); + + return $sr; + } + + public function canConfirm(ServiceRequest $sr): bool + { + return $this->serviceRequestStatusStateMachine->can($sr, ServiceRequestStatusTransition::CONFIRM->value); + } + + public function confirm(ServiceRequest $sr): ServiceRequest + { + if (!$this->canConfirm($sr)) { + throw $this->getException($sr, ServiceRequestStatusTransition::CONFIRM); + } + + $this->serviceRequestStatusStateMachine->apply($sr, ServiceRequestStatusTransition::CONFIRM->value); + + return $sr; + } + + public function canRefuse(ServiceRequest $sr): bool + { + return $this->serviceRequestStatusStateMachine->can($sr, ServiceRequestStatusTransition::REFUSE->value); + } + + public function refuse(ServiceRequest $sr): ServiceRequest + { + if (!$this->canRefuse($sr)) { + throw $this->getException($sr, ServiceRequestStatusTransition::REFUSE); + } + + $this->serviceRequestStatusStateMachine->apply($sr, ServiceRequestStatusTransition::REFUSE->value); + + return $sr; + } + + public function canFinalize(ServiceRequest $sr): bool + { + return $this->serviceRequestStatusStateMachine->can($sr, ServiceRequestStatusTransition::FINALIZE->value); + } + + public function finalize(ServiceRequest $sr): ServiceRequest + { + if (!$this->canFinalize($sr)) { + throw $this->getException($sr, ServiceRequestStatusTransition::FINALIZE); + } + + $this->serviceRequestStatusStateMachine->apply($sr, ServiceRequestStatusTransition::FINALIZE->value); + + return $sr; + } + + public function canAutoFinalize(ServiceRequest $sr): bool + { + return $this->serviceRequestStatusStateMachine->can($sr, ServiceRequestStatusTransition::AUTO_FINALIZE->value); + } + + public function autoFinalize(ServiceRequest $sr): ServiceRequest + { + if (!$this->canAutoFinalize($sr)) { + throw $this->getException($sr, ServiceRequestStatusTransition::AUTO_FINALIZE); + } + + $this->serviceRequestStatusStateMachine->apply($sr, ServiceRequestStatusTransition::AUTO_FINALIZE->value); + + return $sr; + } +} diff --git a/symfony.lock b/symfony.lock new file mode 100644 index 0000000..2073928 --- /dev/null +++ b/symfony.lock @@ -0,0 +1,589 @@ +{ + "api-platform/core": { + "version": "3.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.0", + "ref": "0330386d716d3eecc52ee5ac66976e733eb8f961" + }, + "files": [ + "config/routes/api_platform.yaml", + "src/ApiResource/.gitignore" + ] + }, + "craue/formflow-bundle": { + "version": "3.6.0" + }, + "doctrine/annotations": { + "version": "1.13", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.10", + "ref": "64d8583af5ea57b7afa4aba4b159907f3a148b05" + } + }, + "doctrine/doctrine-bundle": { + "version": "2.7", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.4", + "ref": "da713d006953b90d1c085c1be480ecdd6c4a95e0" + }, + "files": [ + "config/packages/doctrine.yaml", + "src/Entity/.gitignore", + "src/Repository/.gitignore" + ] + }, + "doctrine/doctrine-migrations-bundle": { + "version": "3.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.1", + "ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33" + }, + "files": [ + "config/packages/doctrine_migrations.yaml", + "migrations/.gitignore" + ] + }, + "easycorp/easyadmin-bundle": { + "version": "4.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.0", + "ref": "b131e6cbfe1b898a508987851963fff485986285" + } + }, + "friendsofphp/php-cs-fixer": { + "version": "3.12", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.0", + "ref": "be2103eb4a20942e28a6dd87736669b757132435" + }, + "files": [ + ".php-cs-fixer.dist.php" + ] + }, + "friendsofsymfony/ckeditor-bundle": { + "version": "2.4", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "2.0", + "ref": "f5ad42002183a6881962683e6d84bbb25cdfce5d" + }, + "files": [ + "config/packages/fos_ckeditor.yaml" + ] + }, + "hautelook/alice-bundle": { + "version": "2.11", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.2", + "ref": "c84e4f2b9d7f436d7d52e8369230b393367607ec" + }, + "files": [ + "config/packages/hautelook_alice.yaml", + "fixtures/.gitignore" + ] + }, + "knplabs/knp-paginator-bundle": { + "version": "v6.1.1" + }, + "league/flysystem-bundle": { + "version": "3.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "913dc3d7a5a1af0d2b044c5ac3a16e2f851d7380" + }, + "files": [ + "config/packages/flysystem.yaml", + "var/storage/.gitignore" + ] + }, + "nelmio/alice": { + "version": "3.12", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.3", + "ref": "42b52d2065dc3fde27912d502c18ca1926e35ae2" + }, + "files": [ + "config/packages/nelmio_alice.yaml" + ] + }, + "nelmio/cors-bundle": { + "version": "2.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.5", + "ref": "6bea22e6c564fba3a1391615cada1437d0bde39c" + }, + "files": [ + "config/packages/nelmio_cors.yaml" + ] + }, + "nyholm/psr7": { + "version": "1.5", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "0cd4d2d0e7f646fda75f9944f747a56e6ed13d4c" + }, + "files": [ + "config/packages/nyholm_psr7.yaml" + ] + }, + "odolbeau/phone-number-bundle": { + "version": "3.9", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "3.0", + "ref": "4388686329b81291918a948cd42891829fb1de71" + }, + "files": [ + "config/packages/misd_phone_number.yaml" + ] + }, + "payum/payum-bundle": { + "version": "2.5", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "2.4", + "ref": "518ac22defa04a8a1d82479ed362e2921487adf0" + }, + "files": [ + "config/packages/payum.yaml" + ] + }, + "php-http/discovery": { + "version": "1.18", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.18", + "ref": "f45b5dd173a27873ab19f5e3180b2f661c21de02" + }, + "files": [ + "config/packages/http_discovery.yaml" + ] + }, + "phpunit/phpunit": { + "version": "9.5", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "9.3", + "ref": "a6249a6c4392e9169b87abf93225f7f9f59025e6" + }, + "files": [ + ".env.test", + "phpunit.xml.dist", + "tests/bootstrap.php" + ] + }, + "sensio/framework-extra-bundle": { + "version": "6.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.2", + "ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b" + }, + "files": [ + "config/packages/sensio_framework_extra.yaml" + ] + }, + "snc/redis-bundle": { + "version": "4.3", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "2.0", + "ref": "36b3d9ab65be62de4e085a25e6ca899efa96b1f3" + }, + "files": [ + "config/packages/snc_redis.yaml" + ] + }, + "stof/doctrine-extensions-bundle": { + "version": "1.7", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.2", + "ref": "e805aba9eff5372e2d149a9ff56566769e22819d" + }, + "files": [ + "config/packages/stof_doctrine_extensions.yaml" + ] + }, + "symfony/console": { + "version": "6.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "da0c8be8157600ad34f10ff0c9cc91232522e047" + }, + "files": [ + "bin/console" + ] + }, + "symfony/debug-bundle": { + "version": "6.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "5aa8aa48234c8eb6dbdd7b3cd5d791485d2cec4b" + }, + "files": [ + "config/packages/debug.yaml" + ] + }, + "symfony/fake-sms-notifier": { + "version": "6.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "70edf4dc13e3082d555575325207f0529ddd3bfe" + } + }, + "symfony/flex": { + "version": "2.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "146251ae39e06a95be0fe3d13c807bcf3938b172" + }, + "files": [ + ".env" + ] + }, + "symfony/framework-bundle": { + "version": "6.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.4", + "ref": "3cd216a4d007b78d8554d44a5b1c0a446dab24fb" + }, + "files": [ + "config/packages/cache.yaml", + "config/packages/framework.yaml", + "config/preload.php", + "config/routes/framework.yaml", + "config/services.yaml", + "public/index.php", + "src/Controller/.gitignore", + "src/Kernel.php" + ] + }, + "symfony/google-mailer": { + "version": "6.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "4.4", + "ref": "f8fd4ddb9b477510f8f4bce2b9c054ab428c0120" + } + }, + "symfony/mailer": { + "version": "6.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "4.3", + "ref": "97a61eabb351d7f6cb7702039bcfe07fe9d7e03c" + }, + "files": [ + "config/packages/mailer.yaml" + ] + }, + "symfony/maker-bundle": { + "version": "1.47", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" + } + }, + "symfony/mercure-bundle": { + "version": "0.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "0.3", + "ref": "7c7e63c36530052a174f28a6be4e451c4709be83" + }, + "files": [ + "config/packages/mercure.yaml" + ] + }, + "symfony/messenger": { + "version": "6.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.0", + "ref": "2523f7d31488903e247a522e760dc279be7f7aaf" + }, + "files": [ + "config/packages/messenger.yaml" + ] + }, + "symfony/monolog-bundle": { + "version": "3.8", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.7", + "ref": "213676c4ec929f046dfde5ea8e97625b81bc0578" + }, + "files": [ + "config/packages/monolog.yaml" + ] + }, + "symfony/notifier": { + "version": "6.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.0", + "ref": "c31585e252b32fe0e1f30b1f256af553f4a06eb9" + }, + "files": [ + "config/packages/notifier.yaml" + ] + }, + "symfony/ovh-cloud-notifier": { + "version": "6.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.1", + "ref": "71809b3735a012a6359e9f48aa373b2680c3da54" + } + }, + "symfony/panther": { + "version": "2.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "877d853a05c6713665a2f4b954734592680abff6" + } + }, + "symfony/phpunit-bridge": { + "version": "6.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "97cb3dc7b0f39c7cfc4b7553504c9d7b7795de96" + }, + "files": [ + ".env.test", + "bin/phpunit", + "phpunit.xml.dist", + "tests/bootstrap.php" + ] + }, + "symfony/requirements-checker": { + "version": "2.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.0", + "ref": "02c6e4b9b117c39e8a23eab7f3840ef6e62293b9" + } + }, + "symfony/routing": { + "version": "6.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.1", + "ref": "a44010c0d06989bd4f154aa07d2542d47caf5b83" + }, + "files": [ + "config/packages/routing.yaml", + "config/routes.yaml" + ] + }, + "symfony/security-bundle": { + "version": "6.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.0", + "ref": "8a5b112826f7d3d5b07027f93786ae11a1c7de48" + }, + "files": [ + "config/packages/security.yaml" + ] + }, + "symfony/translation": { + "version": "6.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "da64f5a2b6d96f5dc24914517c0350a5f91dee43" + }, + "files": [ + "config/packages/translation.yaml", + "translations/.gitignore" + ] + }, + "symfony/twig-bundle": { + "version": "6.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.4", + "ref": "bb2178c57eee79e6be0b297aa96fc0c0def81387" + }, + "files": [ + "config/packages/twig.yaml", + "templates/base.html.twig" + ] + }, + "symfony/twilio-notifier": { + "version": "6.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.0", + "ref": "eb697fe6a90b4972a4a544b4f7ea56520d29d9e9" + } + }, + "symfony/uid": { + "version": "6.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.2", + "ref": "d294ad4add3e15d7eb1bae0221588ca89b38e558" + }, + "files": [ + "config/packages/uid.yaml" + ] + }, + "symfony/ux-autocomplete": { + "version": "2.7", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.6", + "ref": "07d9602b7231ba355f484305d6cea58310c01741" + }, + "files": [ + "config/routes/ux_autocomplete.yaml" + ] + }, + "symfony/validator": { + "version": "6.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "c32cfd98f714894c4f128bb99aa2530c1227603c" + }, + "files": [ + "config/packages/validator.yaml" + ] + }, + "symfony/web-profiler-bundle": { + "version": "6.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.1", + "ref": "e42b3f0177df239add25373083a564e5ead4e13a" + }, + "files": [ + "config/packages/web_profiler.yaml", + "config/routes/web_profiler.yaml" + ] + }, + "symfony/webpack-encore-bundle": { + "version": "1.16", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.10", + "ref": "5878c7c28468ca0fdce2497a04cfc66bab0dc3ef" + }, + "files": [ + "assets/app.js", + "assets/stimulus.js", + "assets/controllers.json", + "assets/controllers/hello_controller.js", + "assets/styles/app.css", + "config/packages/webpack_encore.yaml", + "package.json", + "webpack.config.js" + ] + }, + "symfony/workflow": { + "version": "6.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.3", + "ref": "3b2f8ca32a07fcb00f899649053943fa3d8bbfb6" + }, + "files": [ + "config/packages/workflow.yaml" + ] + }, + "theofidry/alice-data-fixtures": { + "version": "1.6", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "fe5a50faf580eb58f08ada2abe8afbd2d4941e05" + } + }, + "twig/extra-bundle": { + "version": "v3.4.0" + }, + "willdurand/geocoder-bundle": { + "version": "5.18", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "5.0", + "ref": "b272ad4fbfcd45a20e7cbfdc4ad1b0e27a62fd3b" + }, + "files": [ + "config/packages/bazinga_geocoder.yaml" + ] + }, + "zenstruck/messenger-test": { + "version": "v1.5.1" + } +} diff --git a/templates/admin/dashboard.html.twig b/templates/admin/dashboard.html.twig new file mode 100644 index 0000000..0e66af8 --- /dev/null +++ b/templates/admin/dashboard.html.twig @@ -0,0 +1,29 @@ +{# https://symfony.com/bundles/EasyAdminBundle/current/dashboards.html#content-page-template #} +{% extends '@EasyAdmin/page/content.html.twig' %} + +{% trans_default_domain 'admin' %} + +{% block content_title %}Dashboard{% endblock %} + +{% block page_actions %} + {# + Some Action + #} +{% endblock %} + +{% block main %} +

{{ 'dashboard.statistics'|trans }}

+ + + + + + + + + + + + +
Nombre de groupes
{{ group_count }}
+{% endblock %} diff --git a/templates/admin/dev/dev_tools.html.twig b/templates/admin/dev/dev_tools.html.twig new file mode 100644 index 0000000..8a0868f --- /dev/null +++ b/templates/admin/dev/dev_tools.html.twig @@ -0,0 +1,32 @@ +{% extends '@EasyAdmin/page/content.html.twig' %} + +{% trans_default_domain 'admin' %} + +{% block content_title %}Tools{% endblock %} + +{% block main %} +

UUid

+
    +
  • Uuid V4 : {{ uuidV4 }}
  • +
  • Uuid V6 : {{ uuidV6 }} ({{ uuidV6.dateTime|date(constant('\DateTime::ATOM')) }})
  • +
+ +

Encoding

+
    +
  • Encoded url of {{ encoded }} : {{ urlEncoded }}
  • +
+ + +

Random

+
    +
  • bin2hex(random_bytes(25)) : {{ confirmationCode1 }} - taille : {{ confirmationCode1|length }}
  • +
  • ByteString::fromRandom(32) : {{ confirmationCode2 }} - taille : {{ confirmationCode2|length }}
  • +
+ +

Transodes for translation messages

+
    + {% for trans_code in transCodes %} +
  • {{ trans_code }}
  • + {% endfor %} +
+{% endblock %} diff --git a/templates/admin/group/invite.html.twig b/templates/admin/group/invite.html.twig new file mode 100644 index 0000000..08eabdd --- /dev/null +++ b/templates/admin/group/invite.html.twig @@ -0,0 +1,15 @@ +{% extends '@EasyAdmin/page/content.html.twig' %} + +{% trans_default_domain('admin') %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% block content_title %} +

{{ (i18n_prefix ~ '.h1')|trans({'%group%': group.name}) }}

+{% endblock %} + +{% block main %} +

{{ (i18n_prefix ~ '.p1')|trans }}

+ + {{ form(form) }} +{% endblock %} diff --git a/templates/admin/group/user_groups_field.html.twig b/templates/admin/group/user_groups_field.html.twig new file mode 100644 index 0000000..52d3a6d --- /dev/null +++ b/templates/admin/group/user_groups_field.html.twig @@ -0,0 +1,7 @@ +{% trans_default_domain 'admin' %} +{% if field.value.count > 0 %} + {% set group = field.value.first.group %} + +{% endif %} diff --git a/templates/admin/loan/product_field.html.twig b/templates/admin/loan/product_field.html.twig new file mode 100644 index 0000000..f2dddf3 --- /dev/null +++ b/templates/admin/loan/product_field.html.twig @@ -0,0 +1,14 @@ +{% trans_default_domain 'admin' %} +
{{ 'Product'|trans }}
+
+ {{ field.value }} + +
+ +
{{ 'Category'|trans }}
+
{{ field.value.category }}
+ +
{{ 'Status'|trans }}
+
{{ field.value.status.value }}
diff --git a/templates/admin/loan/product_type_field.html.twig b/templates/admin/loan/product_type_field.html.twig new file mode 100644 index 0000000..f51fea7 --- /dev/null +++ b/templates/admin/loan/product_type_field.html.twig @@ -0,0 +1,2 @@ +{% trans_default_domain 'admin' %} +{{ field.value.type.value|trans }} diff --git a/templates/admin/loan/user_field.html.twig b/templates/admin/loan/user_field.html.twig new file mode 100644 index 0000000..76d3a00 --- /dev/null +++ b/templates/admin/loan/user_field.html.twig @@ -0,0 +1,19 @@ +{% trans_default_domain 'admin' %} + {% if not field.value.isPLace() %} +
{{ 'Firstname'|trans }}
+
{{ field.value.firstname }}
+ +
{{ 'Lastname'|trans }}
+
{{ field.value.lastname }}
+ {% else %} +
{{ 'Name'|trans }}
+
{{ field.value.name }}
+ {% endif %} + +
{{ 'Email'|trans }}
+
+ {{ field.value.email }} + +
diff --git a/templates/admin/parameters.html.twig b/templates/admin/parameters.html.twig new file mode 100644 index 0000000..2f7f82b --- /dev/null +++ b/templates/admin/parameters.html.twig @@ -0,0 +1,56 @@ +{# https://symfony.com/bundles/EasyAdminBundle/current/dashboards.html#content-page-template #} +{% extends '@EasyAdmin/page/content.html.twig' %} + +{% trans_default_domain 'admin' %} + +{% block content_title %}{{ 'parameters.content_title'|trans }}{% endblock %} + +{% block page_actions %} + {# + Sauver + #} +{% endblock %} + +{% block main %} + {{ form_start(form) }} +
+

{{ 'parameters.senders.h2'|trans }}

+
+ + {{ form_row(form.notificationsSenderEmail) }} + + {{ form_row(form.notificationsSenderName) }} + + +

{{ 'parameters.contact.h2'|trans }}

+
+ + {{ form_widget(form.contactFormEnabled) }} + + {{ form_row(form.contactFormEmail) }} + + +

{{ 'parameters.groups.h2'|trans }}

+
+ + {{ form_widget(form.groupsEnabled) }} + + {{ form_row(form.groupsCreationMode) }} + + {{ form_widget(form.groupsPaying) }} + + +

{{ 'parameters.confidentiality.h2'|trans }}

+
+ + {{ form_widget(form.confidentialityConversationAdminAccess) }} + + {{ 'parameter.conversationsVisibilityWarning'|trans }} + +
+ {{ form_widget(form.submit) }} +
+
+ {{ form_end(form) }} + +{% endblock %} diff --git a/templates/admin/poc/email_notification.html.twig b/templates/admin/poc/email_notification.html.twig new file mode 100644 index 0000000..2acd402 --- /dev/null +++ b/templates/admin/poc/email_notification.html.twig @@ -0,0 +1,17 @@ +{% extends '@EasyAdmin/page/content.html.twig' %} + +{% block main %} +

POC : test d'envoi d'une notification par email

+ +

Envoi avec le Symfony mailer

+ + + +
+ {{ form_start(form, {'attr': {}}) }} + {{ form_widget(form) }} + {{ form_end(form) }} +
+{% endblock %} diff --git a/templates/admin/poc/geoloc.html.twig b/templates/admin/poc/geoloc.html.twig new file mode 100644 index 0000000..7a45d04 --- /dev/null +++ b/templates/admin/poc/geoloc.html.twig @@ -0,0 +1,51 @@ +{% extends '@EasyAdmin/page/content.html.twig' %} + +{% block main %} +

POC : test de géolocalisation avec le bundle

+ +

Résultats avec le fournisseur gratuit Nominatim

+ +
+ {{ form_start(form, {'attr': {}}) }} + {{ form_widget(form) }} + {{ form_end(form) }} +
+ +{% if form.vars.submitted %} + {% if address is not null %} +

Adresse renseignée : {{ address }}

+ {% endif %} + + {% if addressCollection is not null %} + + {% if addressCollection|length %} +

Veuillez confirmer l'adresse

+ {% endif %} + + {# @var Geocoder\Model\Address address #} + {% for address in addressCollection %} +

» Proposition n° {{ loop.index }}

+
    +
  • Numéro : {{ address.streetNumber }}
  • +
  • Rue / voie : {{ address.streetName }}
  • +
  • Localité : {{ address.locality }}
  • +
  • Code postal : {{ address.postalCode }}
  • + {% if address.country is not null %} +
  • Pays : {{ address.country.name }} ({{ address.country.code }})
  • + {% endif %} + + {% if address.adminLevels is not null %} +
  • Régions : {% for admin_level in address.adminLevels %}{{ admin_level.name }} {% endfor %}
  • + {% endif %} + {% if address.coordinates is not null %} +
  • latitude : {{ address.coordinates.latitude }}, longitude : {{ address.coordinates.longitude }}
  • + {% endif %} +
  • Adresse reformatée : {{ address.displayName }}
  • +
+ {% else %} +

Aucune proposition trouvée, veuillez affiner l'adresse.

+ {% endfor %} + {% endif %} +{% endif %} + +{% endblock %} diff --git a/templates/admin/poc/meilisearch.html.twig b/templates/admin/poc/meilisearch.html.twig new file mode 100644 index 0000000..d5d12e4 --- /dev/null +++ b/templates/admin/poc/meilisearch.html.twig @@ -0,0 +1,13 @@ +{# https://symfony.com/bundles/EasyAdminBundle/current/dashboards.html#content-page-template #} +{% extends '@EasyAdmin/page/content.html.twig' %} + +{% trans_default_domain 'admin' %} + +{% block content_title %}Dashboard{% endblock %} + +{% block page_actions %} +{% endblock %} + +{% block main %} +

Meilisearch

+{% endblock %} diff --git a/templates/admin/poc/sms_notification.html.twig b/templates/admin/poc/sms_notification.html.twig new file mode 100644 index 0000000..e978dc3 --- /dev/null +++ b/templates/admin/poc/sms_notification.html.twig @@ -0,0 +1,19 @@ +{% extends '@EasyAdmin/page/content.html.twig' %} + +{% block main %} +

POC : test d'envoi d'une notification SMS

+ +

Envoi avec l'API Twilio / OVHCloud

+ + + +
+ {{ form_start(form, {'attr': {}}) }} + {{ form_widget(form) }} + {{ form_end(form) }} +
+{% endblock %} diff --git a/templates/admin/product/availability_product.html.twig b/templates/admin/product/availability_product.html.twig new file mode 100644 index 0000000..c1e5684 --- /dev/null +++ b/templates/admin/product/availability_product.html.twig @@ -0,0 +1,69 @@ +{% extends '@EasyAdmin/page/content.html.twig' %} + +{% trans_default_domain 'admin' %} + +{% set product = ea.entity.instance %} + +{% block page_actions %} + {% include 'easy_admin/button/back.html.twig' %} +{% endblock %} + +{% block content_title %}{{ 'product.availability'|trans ~ ' - ' ~ product.name }}{% endblock %} + +{% block main %} +
+
+

{{ product.type.value|trans }}

+
+
+
+ {{ product.name }} + {{ product.category }} + {{ product.status.name|trans }} +
+ {% if product.images is not null %} + {% for image in product.images %} + + {% endfor %} + {% elseif product.category.image %} + + {% else %} + + {% endif %} +
+
+ +
+

{{ 'owner'|trans }}

+
+
+
+ {{ product.owner.firstname }} {{ product.owner.lastname }} + {{ product.owner.email }} +
+ {% if product.owner.avatar is not null %} + + {% else %} + + {% endif %} +
+
+
+ + {% set unavailabilities = product.unavailabilities %} + {% if unavailabilities is not empty %} +
+
+

{{ 'product.unavailabilities'|trans }}

+
+
+
+ {% for date in unavailabilities %} + {{ date(date)|date('d/m/Y') }} + {% endfor %} +
+
+
+
+ {% endif %} +{% endblock %} diff --git a/templates/admin/service_request/conversation.html.twig b/templates/admin/service_request/conversation.html.twig new file mode 100644 index 0000000..00729dd --- /dev/null +++ b/templates/admin/service_request/conversation.html.twig @@ -0,0 +1,40 @@ +{# https://symfony.com/bundles/EasyAdminBundle/current/dashboards.html#content-page-template #} +{% extends '@EasyAdmin/page/content.html.twig' %} + +{% trans_default_domain 'admin' %} + +{% block content_title %}{{ 'conversation.title'|trans({'%product%': sr.product}) }}{% endblock %} + +{% block page_actions %} + {% include 'easy_admin/button/back.html.twig' %} +{% endblock %} + +{# Get entity from context #} +{% set sr = ea.entity.instance %} + +{% block main %} +
+

{{ 'Conversation'|trans }}

+ + {% for message in sr.messages %} +
+
+ {{ ('message.type.' ~ message.type.value)|trans({}, 'messages') }} + {{ 'date.the'|trans }} {{ message.createdAt|date('d/m/Y') }} + {{ 'date.time_on'|trans }} {{ message.createdAt|date('H:i:s') }} +
+
+ {% if not message.type.isSystem %} + {% set sender = message.getSender() %} + {{ 'user.by'|trans }} + + {{ sender.displayName }} - {{ sender.email }} + +
+ {% endif %} +

{{ message.message }}

+
+
+
+ {% endfor %} +{% endblock %} diff --git a/templates/bundles/TwigBundle/Exception/error.html.twig b/templates/bundles/TwigBundle/Exception/error.html.twig new file mode 100644 index 0000000..76f3b24 --- /dev/null +++ b/templates/bundles/TwigBundle/Exception/error.html.twig @@ -0,0 +1,17 @@ +{% extends 'layout/base.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% block body %} +
+ +
{{ (i18n_prefix ~ '.h5')|trans({'%status_code%': status_code}) }}
+
+ {% if status_code in [403, 404] %} +

{{ (i18n_prefix ~ '.' ~ status_code)|trans }}

+ {% else %} +

{{ (i18n_prefix ~ '.p1')|trans({'%status_code%': status_code, '%status_text%': status_code|status_text}) }}

+ {% endif %} +
+
+{% endblock %} diff --git a/templates/cms/page.html.twig b/templates/cms/page.html.twig new file mode 100644 index 0000000..9c3ef2d --- /dev/null +++ b/templates/cms/page.html.twig @@ -0,0 +1,26 @@ +{% extends 'layout/base.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% block title %}{{ page is not null ? page.name : brand }}{% endblock %} + +{% block body %} +
+ {% if page is not null %} + {{ page.content|raw }} + {% else %} +

{{ (i18n_prefix ~ '.h1')|trans({'%brand%': brand}) }}

+ {% endif %} +
+{% endblock %} +{% block funding %} + {% if is_home|default(false) %} +
+
+
+ funding logo +
+
+
+ {% endif %} +{% endblock %} diff --git a/templates/components/conversation/_action.html.twig b/templates/components/conversation/_action.html.twig new file mode 100644 index 0000000..8017085 --- /dev/null +++ b/templates/components/conversation/_action.html.twig @@ -0,0 +1,44 @@ +
+
+ {% if accept or confirm %} +
+ +
+ {% endif %} + + {% if finalize %} +
+ +
+ {% include 'components/conversation/_modal-finalize.html.twig' with { + finalize + } %} + {% endif %} + + {% if refuse %} +
+ +
+ {% include 'components/conversation/_modal-refuse.html.twig' with { + refuse + } %} + {% endif %} +
+
diff --git a/templates/components/conversation/_avatar.html.twig b/templates/components/conversation/_avatar.html.twig new file mode 100644 index 0000000..4150362 --- /dev/null +++ b/templates/components/conversation/_avatar.html.twig @@ -0,0 +1,15 @@ +
+ {% if contact.avatar is not null %} +
+
+ {% else %} + + {% endif %} +
+

+ + {{ name }} + +

diff --git a/templates/components/conversation/_badge.html.twig b/templates/components/conversation/_badge.html.twig new file mode 100644 index 0000000..91a4ed2 --- /dev/null +++ b/templates/components/conversation/_badge.html.twig @@ -0,0 +1,16 @@ +{% if status.value == "to_confirm" %} + {% set type = "secondary" %} +{% elseif status.value == "confirmed" %} + {% set type = "primary" %} +{% elseif status.value == "refused" %} + {% set type = "danger" %} +{% elseif status.value == "finished" %} + {% set type = "success" %} +{% else %} + {% set type = "warning" %} +{% endif %} + +{% include 'components/utils/_badge.html.twig' with { + type, + name: status.name|trans({}, 'admin')|capitalize +} %} diff --git a/templates/components/conversation/_calendar.html.twig b/templates/components/conversation/_calendar.html.twig new file mode 100644 index 0000000..287b0f7 --- /dev/null +++ b/templates/components/conversation/_calendar.html.twig @@ -0,0 +1,24 @@ +
+
+

+ {% if start_at == end_at %} + {{ 'templates.pages.account.conversation.date'|trans({ + '%date%': start_at, + }) }} + {% else %} + {{ 'templates.pages.account.conversation.dates'|trans({ + '%startAt%': start_at, + '%endAt%': end_at, + }) }} + {% endif %} +

+
+ + {% if workflowCanModifyOwner or workflowCanModifyRecipient %} +
+ +
+ {% endif %} +
diff --git a/templates/components/conversation/_chat.html.twig b/templates/components/conversation/_chat.html.twig new file mode 100644 index 0000000..0c8c8c9 --- /dev/null +++ b/templates/components/conversation/_chat.html.twig @@ -0,0 +1,29 @@ +
+ {% include 'components/layout/_title_5.html.twig' with { + rowClass: 'mb-4', + title: 'templates.pages.account.conversation.messages'|trans + } %} +
+
+
    + {% for message in messages %} +
  • + {% if message.type.isSystem %} +

    + + {{ 'templates.pages.account.conversation.system_message'|trans }} +

    + {% endif %} + +

    + {{ message.message|nl2br }} +

    +

    + {{ message.createdAt|date('d/m/Y - H\\hi') }} +

    +
  • + {% endfor %} +
+
+
+
diff --git a/templates/components/conversation/_modal-confirmation.html.twig b/templates/components/conversation/_modal-confirmation.html.twig new file mode 100644 index 0000000..a63d338 --- /dev/null +++ b/templates/components/conversation/_modal-confirmation.html.twig @@ -0,0 +1,88 @@ + diff --git a/templates/components/conversation/_modal-finalize.html.twig b/templates/components/conversation/_modal-finalize.html.twig new file mode 100644 index 0000000..d619db5 --- /dev/null +++ b/templates/components/conversation/_modal-finalize.html.twig @@ -0,0 +1,39 @@ + + diff --git a/templates/components/conversation/_modal-refuse.html.twig b/templates/components/conversation/_modal-refuse.html.twig new file mode 100644 index 0000000..7a0bffa --- /dev/null +++ b/templates/components/conversation/_modal-refuse.html.twig @@ -0,0 +1,38 @@ + + + diff --git a/templates/components/conversation/_product-info.html.twig b/templates/components/conversation/_product-info.html.twig new file mode 100644 index 0000000..596e7ec --- /dev/null +++ b/templates/components/conversation/_product-info.html.twig @@ -0,0 +1,25 @@ +
+
+ {% if product.images is not empty %} + {{ product.name }} + {% elseif category.image %} + {{ product.name }} + {% else %} + {{ product.name }} + {% endif %} +
+
+
+ {{ product.name }} +
+

+ {% if category.parent %} + {{ category.parent }}  /  + {% endif %} + {{ category }} +

+
+
diff --git a/templates/components/footer/_links.html.twig b/templates/components/footer/_links.html.twig new file mode 100644 index 0000000..3cb9ec8 --- /dev/null +++ b/templates/components/footer/_links.html.twig @@ -0,0 +1,36 @@ +{% if links|length > 3 %} + {% for item in links|batch(3) %} +
+ +
+ {% endfor %} +{% else %} +
+ +
+{% endif %} +
+ +
diff --git a/templates/components/footer/_logo.html.twig b/templates/components/footer/_logo.html.twig new file mode 100644 index 0000000..9f2acd1 --- /dev/null +++ b/templates/components/footer/_logo.html.twig @@ -0,0 +1,7 @@ +
+ {% if menu.logo is null %} + + {% else %} + logo + {% endif %} +
diff --git a/templates/components/footer/_social_network.html.twig b/templates/components/footer/_social_network.html.twig new file mode 100644 index 0000000..898ffbf --- /dev/null +++ b/templates/components/footer/_social_network.html.twig @@ -0,0 +1,12 @@ +{% set items = icons|sort((a, b) => a.position <=> b.position) %} + diff --git a/templates/components/form/_address_form_theme.html.twig b/templates/components/form/_address_form_theme.html.twig new file mode 100644 index 0000000..d65e2ee --- /dev/null +++ b/templates/components/form/_address_form_theme.html.twig @@ -0,0 +1,42 @@ +{# @see vendor/symfony/twig-bridge/Resources/views/Form/bootstrap_5_layout.html.twig #} +{% use 'bootstrap_5_layout.html.twig' %} + +{%- block choice_widget_expanded -%} +
+
    + {%- for child in form %} + {% set address = child.parent.vars.choices[child.vars.value].data %} +
  • +
    +
    + {{- form_widget(child, { + label: false + }) -}} +
    +
    +

    + {{ address.streetNumber }} {{ address.streetName }} +
    + {{ address.postalCode }} {{ address.locality }} +
    + {% if address.country is not null %} + {{ address.country.name }} + {% endif %} +
    + {% if address.coordinates is not null and address.osmType is not empty and address.osmId is not empty %} + + {{ address.coordinates.latitude }}, {{ address.coordinates.longitude }} + + {% endif %} +

    + ({{ address.displayName }}) +

    +
    +
    +
  • + {% endfor -%} +
+
+{%- endblock choice_widget_expanded %} diff --git a/templates/components/form/_password_visibility.html.twig b/templates/components/form/_password_visibility.html.twig new file mode 100644 index 0000000..61e0884 --- /dev/null +++ b/templates/components/form/_password_visibility.html.twig @@ -0,0 +1,43 @@ +{% trans_default_domain 'security' %} + +
+ {% if form is defined and not null %} + {{ form_label(form) }} + {% else %} + + {% endif %} +
+ {% if form is defined and not null %} + {{ form_widget(form) }} + {% else %} + + {% endif %} +
+ + + + +
+ {% if form is defined and not null %} + {{ form_errors(form) }} + {% endif %} +
+ {% if form is defined and not null %} + {{ form_help(form) }} + {% endif %} +
diff --git a/templates/components/form/_photo_preview.html.twig b/templates/components/form/_photo_preview.html.twig new file mode 100644 index 0000000..8e6de15 --- /dev/null +++ b/templates/components/form/_photo_preview.html.twig @@ -0,0 +1,11 @@ +{% if isUser is defined and isUser == true %} + +{% else %} + +{% endif %} diff --git a/templates/components/form/_select.html.twig b/templates/components/form/_select.html.twig new file mode 100644 index 0000000..89c9210 --- /dev/null +++ b/templates/components/form/_select.html.twig @@ -0,0 +1,5 @@ + +{{ form_start(form) }} + {{ form_widget(form.category, {'attr': {'onChange': 'submit()'}}) }} + {{ form_widget(form.submit, {'attr': {'class': 'hidden'}}) }} +{{ form_end(form) }} diff --git a/templates/components/group/_card.html.twig b/templates/components/group/_card.html.twig new file mode 100644 index 0000000..e8cd9b6 --- /dev/null +++ b/templates/components/group/_card.html.twig @@ -0,0 +1,20 @@ +{% set pill_bg = group.type.isPublic() ? 'text-primary text-bg-blue-custom' : 'text-bg-secondary' %} + +
+
+ + {{ group.name }} + + + {{ (i18n_prefix ~ '.pill_' ~ group.type.value)|trans }} + +
+ +
+ diff --git a/templates/components/group/_first_offer.html.twig b/templates/components/group/_first_offer.html.twig new file mode 100644 index 0000000..8340255 --- /dev/null +++ b/templates/components/group/_first_offer.html.twig @@ -0,0 +1,19 @@ +{% set first_offer = group.activeOffers|first %} +
+

+ {{ (i18n_prefix ~ '.membership')|trans }} + + {{ (i18n_prefix ~ '.membership.start')|trans }} + {{ first_offer.actualPrice|format_currency(first_offer.currency) }} + +

+
+
+ +
diff --git a/templates/components/group/_list-content.html.twig b/templates/components/group/_list-content.html.twig new file mode 100644 index 0000000..bc9cde1 --- /dev/null +++ b/templates/components/group/_list-content.html.twig @@ -0,0 +1,26 @@ +{% set pill_bg = item.group.type.isPublic() ? 'text-primary text-bg-blue-custom' : 'text-bg-secondary' %} +{% set link = isAdmin ? ea_url().setController('App\\Controller\\Admin\\GroupCrudController').setAction('detail').setEntityId(item.group.id) : path('app_group_show', {slug: item.group.slug, id: item.group.id}) %} +{% set button_action = isAdmin ? (i18n_prefix ~ '.admin')|trans : (i18n_prefix ~ '.join')|trans %} + +{% if item %} + +
+
+ + {{ item.group }} + + + {{ (i18n_prefix ~ '.' ~ item.group.type.value)|trans }} + +
+ {% if needAction|default(false) %} +
+ + {{ button_action }} + +
+ {% endif %} +
+
+{% endif %} diff --git a/templates/components/group/_member_card.html.twig b/templates/components/group/_member_card.html.twig new file mode 100644 index 0000000..8a6cb09 --- /dev/null +++ b/templates/components/group/_member_card.html.twig @@ -0,0 +1,36 @@ +
+
+ {# avatar of the member #} +
+ {% if user.avatar is not null %} + Avatar + {% else %} + + {% endif %} +
+
+ {% if not isInvited %} +
+ {{ user.displayName }} +
+ {% endif %} + {# category of the member or badge of the invited user #} + {% if category is not null %} +

+ {% if category.parent %} + {{ category.parent }}  /  + {% endif %} + {{ category }} +

+ {% endif %} + {% if isInvited %} +
+ {% include 'components/utils/_badge.html.twig' with { + type: 'invited member', + name: (i18n_prefix ~ '.pending_invitation')|trans + } %} +
+ {% endif %} +
+
+
diff --git a/templates/components/group/_modal.html.twig b/templates/components/group/_modal.html.twig new file mode 100644 index 0000000..1919809 --- /dev/null +++ b/templates/components/group/_modal.html.twig @@ -0,0 +1,54 @@ + diff --git a/templates/components/group/_modal_offers.html.twig b/templates/components/group/_modal_offers.html.twig new file mode 100644 index 0000000..3d3f2b7 --- /dev/null +++ b/templates/components/group/_modal_offers.html.twig @@ -0,0 +1,49 @@ + diff --git a/templates/components/item/_conversation.html.twig b/templates/components/item/_conversation.html.twig new file mode 100644 index 0000000..71d9fc9 --- /dev/null +++ b/templates/components/item/_conversation.html.twig @@ -0,0 +1,39 @@ + +
+
+ {% if product.images is not empty %} + {{ product_name }} + {% elseif product.category.image is not empty %} + {{ product_name }} + {% else %} + {{ product_name }} + {% endif %} +
+
+
{{ name }}
+

{{ product_name }} - {{ date|date('d/m/Y') }}

+
+ {% include 'components/conversation/_badge.html.twig' with { + status + } %} +
+
+
+
+ + {% if has_message %} + + {{ 'conversations.list.new_messages'|trans }} + + {% endif %} +
+
+
+ +
+
+
diff --git a/templates/components/item/_product.html.twig b/templates/components/item/_product.html.twig new file mode 100644 index 0000000..1a62660 --- /dev/null +++ b/templates/components/item/_product.html.twig @@ -0,0 +1,122 @@ +{% set i18n_prefix = _self|i18n_prefix %} + +
+
+ +
+
+ {{ product.name }} +
+

+ {% if category.parent %} + {{ category.parent }}  /  + {% endif %} + {{ category }} +

+
+ {% include 'components/utils/_badge.html.twig' with { + type: 'success', + name: (i18n_prefix ~ '.active')|trans + } %} +
+
+ {% include 'components/utils/_badge.html.twig' with { + type: 'secondary', + name: (i18n_prefix ~ '.paused')|trans + } %} +
+
+
+ {% if has_conversation %} + + + + {% endif %} +
+
+ +
+
+
diff --git a/templates/components/layout/_back_to_link.html.twig b/templates/components/layout/_back_to_link.html.twig new file mode 100644 index 0000000..7e5eed3 --- /dev/null +++ b/templates/components/layout/_back_to_link.html.twig @@ -0,0 +1,13 @@ + diff --git a/templates/components/layout/_footer.html.twig b/templates/components/layout/_footer.html.twig new file mode 100644 index 0000000..4bda24e --- /dev/null +++ b/templates/components/layout/_footer.html.twig @@ -0,0 +1,5 @@ +
+ {% include 'components/footer/_logo.html.twig' with {menu} %} + {% include 'components/footer/_links.html.twig' with links %} + {% include 'components/footer/_social_network.html.twig' with icons %} +
diff --git a/templates/components/layout/_multiple_searchbar.html.twig b/templates/components/layout/_multiple_searchbar.html.twig new file mode 100644 index 0000000..584419e --- /dev/null +++ b/templates/components/layout/_multiple_searchbar.html.twig @@ -0,0 +1,5 @@ + +{{ form_start(form) }} + {{ form_widget(form.product, {'attr': {'onChange': 'submit()'}}) }} + {{ form_widget(form.submit, {'attr': {'class': 'hidden'}}) }} +{{ form_end(form) }} diff --git a/templates/components/layout/_navbar.html.twig b/templates/components/layout/_navbar.html.twig new file mode 100644 index 0000000..6d45702 --- /dev/null +++ b/templates/components/layout/_navbar.html.twig @@ -0,0 +1,127 @@ +{% set logged = is_granted('IS_AUTHENTICATED_REMEMBERED') %} +{% set path = path('app_user_my_account') %} + + diff --git a/templates/components/layout/_pagination.html.twig b/templates/components/layout/_pagination.html.twig new file mode 100644 index 0000000..efc3cf5 --- /dev/null +++ b/templates/components/layout/_pagination.html.twig @@ -0,0 +1,5 @@ +{% if pagination is defined %} +
+ {{ knp_pagination_render(pagination) }} +
+{% endif %} diff --git a/templates/components/layout/_searchbar.html.twig b/templates/components/layout/_searchbar.html.twig new file mode 100644 index 0000000..8cf8b7c --- /dev/null +++ b/templates/components/layout/_searchbar.html.twig @@ -0,0 +1,22 @@ +
+ {% if form is not defined %} +
+ {% if label is defined %} + + {% endif %} +
+ + +
+
+ {% else %} + {{ form_start(form) }} +
+ {{ form_widget(form.q) }} + {{ form_widget(form.submit) }} +
+ {{ form_end(form) }} + {% endif %} +
diff --git a/templates/components/layout/_text.html.twig b/templates/components/layout/_text.html.twig new file mode 100644 index 0000000..1617e23 --- /dev/null +++ b/templates/components/layout/_text.html.twig @@ -0,0 +1,5 @@ +
+
+

{{ text|raw|nl2br }}

+
+
diff --git a/templates/components/layout/_title_3.html.twig b/templates/components/layout/_title_3.html.twig new file mode 100644 index 0000000..e2189bc --- /dev/null +++ b/templates/components/layout/_title_3.html.twig @@ -0,0 +1,5 @@ +
+
+

{{ name }}

+
+
diff --git a/templates/components/layout/_title_5.html.twig b/templates/components/layout/_title_5.html.twig new file mode 100644 index 0000000..f01b802 --- /dev/null +++ b/templates/components/layout/_title_5.html.twig @@ -0,0 +1,5 @@ +
+
+
{{ title }}
+
+
diff --git a/templates/components/list/_conversations.html.twig b/templates/components/list/_conversations.html.twig new file mode 100644 index 0000000..207fe6f --- /dev/null +++ b/templates/components/list/_conversations.html.twig @@ -0,0 +1,15 @@ +
    + {% for conversation in pagination %} +
  • + {% include 'components/item/_conversation.html.twig' with { + id: conversation.id, + name: isMyLoans ? conversation.owner.displayName : conversation.recipient.displayName, + product_name: conversation.product.name, + has_message: conversation.hasUnreadMessages(app.user), + date: conversation.startAt, + status: conversation.status, + product: conversation.product + } %} +
  • + {% endfor %} +
diff --git a/templates/components/list/_products.html.twig b/templates/components/list/_products.html.twig new file mode 100644 index 0000000..622d073 --- /dev/null +++ b/templates/components/list/_products.html.twig @@ -0,0 +1,12 @@ +
    + {% for product in products %} +
  • + {% include 'components/item/_product.html.twig' with { + product, + category: product.getCategory(), + route, + has_conversation: product.hasServiceRequests(), + } %} +
  • + {% endfor %} +
diff --git a/templates/components/product/_address.html.twig b/templates/components/product/_address.html.twig new file mode 100644 index 0000000..4db0ad7 --- /dev/null +++ b/templates/components/product/_address.html.twig @@ -0,0 +1,19 @@ +{% if address is not null %} +
+
+ {{ 'product.form.address'|trans ~ ' : ' }} +
+

{{ address.getAddress() }},

+

{{ address.postalCode }} {{ address.subAndlocality }}

+
+
+{% endif %} +{% if schedule is not empty %} +
+
+ {{ 'product.form.schedules'|trans ~ ' : ' }} +
+

{{ schedule }}

+
+
+{% endif %} diff --git a/templates/components/product/_address_form.html.twig b/templates/components/product/_address_form.html.twig new file mode 100644 index 0000000..0d1e669 --- /dev/null +++ b/templates/components/product/_address_form.html.twig @@ -0,0 +1,11 @@ +{% if app.user.address is not null %} + {% include 'components/layout/_title_5.html.twig' with { + rowClass: 'border-top', + title: 'product.form.recover'|trans, + colClass: 'col-md-8 col-lg-6 col-xl-5 mt-4' + } %} + {% include 'components/product/_address.html.twig' with { + address: app.user.address, + schedule: app.user.schedule + } %} +{% endif %} diff --git a/templates/components/product/_calendar.html.twig b/templates/components/product/_calendar.html.twig new file mode 100644 index 0000000..cd07aeb --- /dev/null +++ b/templates/components/product/_calendar.html.twig @@ -0,0 +1,96 @@ +{% set is_product_owner = product.owner == app.user %} + +
+
+
+
{{ title }}
+
+
+

{{ 'templates.components.product.calendar.clarifying_sentence'|trans }}

+
+
+ {% if is_product_owner and form is not defined %} + {# user product page #} +
+
+
+ + {% else %} + {% if form is defined %} + {# new service request and product availabilities page #} +
+ {{ form_label(form.startAt, null, {label_attr: {class: 'order-2 mt-4'}}) }} + {{ form_errors(form.startAt) }} + {{ form_widget(form.startAt, {id: 'calendar-start-day', attr: {class: 'order-3'}}) }} +
+
+ {{ form_label(form.endAt, null, {label_attr: {class: 'mt-3'}}) }} + {{ form_errors(form.endAt) }} + {{ form_widget(form.endAt, {id: 'calendar-end-day', type: 'text'}) }} +
+ {% else %} + {# product page #} +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+ {% endif %} +
+ +
+ {% if actionNeeded %} +
+ {% if app.user is null or is_granted('borrow', product) %} + + {% else %} + {{ form_widget(form.submit) }} + {% endif %} +
+ {% endif %} + {% endif %} +
diff --git a/templates/components/product/_card.html.twig b/templates/components/product/_card.html.twig new file mode 100644 index 0000000..3849249 --- /dev/null +++ b/templates/components/product/_card.html.twig @@ -0,0 +1,54 @@ +
+ + + +
+
+ {{ product.name }} +
+

+ {% if product.category.parent is not null %} + {{ product.category.parent }} / {{ product.category }} + {% else %} + {{ product.category }} + {% endif %} +

+ + {% if product.owner.address is not null %} + {{ product.owner.address.subAndLocality }} + + {% if product.geoDistance is not null %} + {{ product.geoDistance|round(1) }} km + {% endif %} + {% endif %} +
+
diff --git a/templates/components/product/_carousel.html.twig b/templates/components/product/_carousel.html.twig new file mode 100644 index 0000000..fc9dba8 --- /dev/null +++ b/templates/components/product/_carousel.html.twig @@ -0,0 +1,26 @@ +
+
+
+
+ {% if product.images is not empty %} + {% for image in product.images %} +
+ {{ product.name }} +
+ {% endfor %} + {% elseif category.getImage() %} +
+ {{ product.name }} +
+ {% else %} +
+ {{ product.name }} +
+ {% endif %} +
+
+
+
+
+
diff --git a/templates/components/product/_images.html.twig b/templates/components/product/_images.html.twig new file mode 100644 index 0000000..cc0658c --- /dev/null +++ b/templates/components/product/_images.html.twig @@ -0,0 +1,11 @@ +
+
{{ 'product.form.images'|trans }}
+

{{ 'product.form.images_default'|trans }}

+ {{ form_row(form, { + attr: { + "data-productupload-target": "input", + "data-action": "productupload#checkUpload" + }}) }} +

{{ 'product.form.upload_multiple'|trans }}

+
+
diff --git a/templates/components/product/_info.html.twig b/templates/components/product/_info.html.twig new file mode 100644 index 0000000..945080d --- /dev/null +++ b/templates/components/product/_info.html.twig @@ -0,0 +1,94 @@ +{% set category = product.getCategory() %} +{% set parent_category = category.getParent() %} + +
+
+
+

{{ product.name }}

+
+
+
+
+

+ {% if parent_category %} + {{ parent_category }}  /  + {% endif %} + {{ category }} +

+
+
+ + {# CAROUSEL #} + {% include 'components/product/_carousel.html.twig' with { + product, + category, + } %} + + {# CITY #} + {% set city = product.owner.address ? product.owner.address.subAndlocality : '' %} + {% if city is not empty %} +
+
+ {{ city }} +
+
+ {% endif %} + + {# DESCRIPTION #} + {% if product.description is not empty %} +
+
+ {% if product.description|length > 150 %} +
+

{{ product.description|nl2br }}

+
+ {{ 'product.info_more'|trans }} + {% else %} +

{{ product.description|nl2br }}

+ {% endif %} +
+
+ {% endif %} + + {# AGE #} + {% if product.age is not null %} +
+
+ {# TODO: Remove this translation form 'security' group #} +

{{ 'product.info_age'|trans }} : {{ product.age }}

+
+
+ {% endif %} + + {# DEPOSIT #} + {% if product.deposit is not null and product.deposit != 0 %} +
+
+

+ {{ 'product.info_deposit'|trans }} : + {{ product.getDepositReal()|format_currency(product.currency) }} +

+
+
+ {% endif %} + + {# DURATION (ONLY SERVICES) #} + {% if product.duration is not null %} +
+
+

{{ 'product.info_duration'|trans }}{{ product.duration }}

+
+
+ {% endif %} + + {# PREFERRED LOAN DURATION (ONLY OBJECTS) #} + {% if product.preferredLoanDuration is not null %} +
+
+

{{ 'product.info_loan_duration'|trans() }}{{ product.preferredLoanDuration }}

+
+
+ {% endif %} +
+ diff --git a/templates/components/product/_lender.html.twig b/templates/components/product/_lender.html.twig new file mode 100644 index 0000000..1ab743f --- /dev/null +++ b/templates/components/product/_lender.html.twig @@ -0,0 +1,38 @@ + diff --git a/templates/components/product/_modal.html.twig b/templates/components/product/_modal.html.twig new file mode 100644 index 0000000..59cc0b5 --- /dev/null +++ b/templates/components/product/_modal.html.twig @@ -0,0 +1,44 @@ +{% if menu_action is defined and menu_action == true %} + {{ button }} +{% else %} +
+ +
+{% endif %} + + diff --git a/templates/components/product/_object_fields.html.twig b/templates/components/product/_object_fields.html.twig new file mode 100644 index 0000000..0d67379 --- /dev/null +++ b/templates/components/product/_object_fields.html.twig @@ -0,0 +1,39 @@ +
+ {{ form_row(form.category) }} + {{ form_row(form.name, { + label: 'object.form.name'|trans + }) }} + {{ form_row(form.description) }} + {{ form_row(form.age) }} + {{ form_row(form.deposit) }} + {{ form_row(form.preferredLoanDuration) }} + {% if form.visibility is defined %} +
+ {% include 'components/product/service_request/_visibility.html.twig' with { + visibility: form.visibility, + product: 'objet' + } %} +
+ {{ form_row(form.groups) }} +
+
+ {% endif %} + {% include 'components/product/_images.html.twig' with { + form: form.images + } %} +
+ +{# + // https://github.com/symfony/webpack-encore-bundle#stimulus_target + {{ form_row(form.images, { + attr: + stimulus_target('controller-name', 'target-name').toArray() | merge( + stimulus_action('controller-name', 'action-name').toArray() ) + }) }} +#} diff --git a/templates/components/product/_search.html.twig b/templates/components/product/_search.html.twig new file mode 100644 index 0000000..ccd8eb2 --- /dev/null +++ b/templates/components/product/_search.html.twig @@ -0,0 +1,95 @@ +{% set i18n_prefix = _self|i18n_prefix %} + +
+
+
+
+
+ {{ form_start(form) }} +
+ +
+ +
+
{{ (i18n_prefix ~ '.accordion.toogle')|trans() }}
+ +
+
+
+
+ {{ form_widget(form.category, { + attr: { + class: 'form-select mt-2 mx-auto', + 'aria-label': 'category' + } + }) }} +
+
+
+
+
+ + + + +
+ {{ form_widget(form.place, { + attr: { + class: 'form-select mt-2 border border-2 mx-auto"', + 'arial-label': 'place' + } + }) }} +
+
+
+
+
+ + + + +
+
+ {{ form_widget(form.city, { + attr: { + class: 'form-control border border-2 mt-2', + placeholder: i18n_prefix ~ '.city.placeholder', + } + }) }} + + + +
+
+
+
+
+ {{ form_widget(form.distance) }} +
+
+
+ {{ form_widget(form.submit) }} +
+
+ +
+
+ {{ form_end(form) }} +
+
+
+
+
diff --git a/templates/components/product/_section.html.twig b/templates/components/product/_section.html.twig new file mode 100644 index 0000000..a06a0a0 --- /dev/null +++ b/templates/components/product/_section.html.twig @@ -0,0 +1,26 @@ +{% set index = 0 %} +
+
+ +
+
diff --git a/templates/components/product/_service_fields.html.twig b/templates/components/product/_service_fields.html.twig new file mode 100644 index 0000000..50e8d21 --- /dev/null +++ b/templates/components/product/_service_fields.html.twig @@ -0,0 +1,28 @@ +
+ {{ form_row(form.category) }} + {{ form_row(form.name, { + label: 'service.form.name'|trans + }) }} + {{ form_row(form.description) }} + {{ form_row(form.duration) }} + {% if form.visibility is defined %} +
+ {% include 'components/product/service_request/_visibility.html.twig' with { + visibility: form.visibility, + product: 'service' + } %} +
+ {{ form_row(form.groups) }} +
+
+ {% endif %} + {% include 'components/product/_images.html.twig' with { + form: form.images + } %} +
diff --git a/templates/components/product/_tab_content.html.twig b/templates/components/product/_tab_content.html.twig new file mode 100644 index 0000000..95a3585 --- /dev/null +++ b/templates/components/product/_tab_content.html.twig @@ -0,0 +1,36 @@ +
+
+
    + {% for product in objects_pagination %} +
  • + {% include 'components/product/_card.html.twig' with {product} %} +
  • + {% endfor %} +
+ {% include 'components/layout/_pagination.html.twig' with { + pagination: objects_pagination, + } %} +
+ {% if services_pagination is defined and services_pagination is not null %} +
+
    + {% for product in services_pagination %} +
  • + {% include 'components/product/_card.html.twig' with {product} %} +
  • + {% endfor %} +
+ {% include 'components/layout/_pagination.html.twig' with { + pagination: services_pagination, + } %} +
+
+{% endif %} diff --git a/templates/components/product/_write_message.html.twig b/templates/components/product/_write_message.html.twig new file mode 100644 index 0000000..b7e1256 --- /dev/null +++ b/templates/components/product/_write_message.html.twig @@ -0,0 +1,13 @@ +
+
+
+ {{ 'templates.components.product.write_message.title'|trans }} +
+
+
+ {{ form_row(form.message) }} +
+
+ {{ form_widget(form.submit) }} +
+
diff --git a/templates/components/product/service_request/_submit.html.twig b/templates/components/product/service_request/_submit.html.twig new file mode 100644 index 0000000..37e2b09 --- /dev/null +++ b/templates/components/product/service_request/_submit.html.twig @@ -0,0 +1,3 @@ +
+ {{ form_widget(submit) }} +
diff --git a/templates/components/product/service_request/_visibility.html.twig b/templates/components/product/service_request/_visibility.html.twig new file mode 100644 index 0000000..4f080ab --- /dev/null +++ b/templates/components/product/service_request/_visibility.html.twig @@ -0,0 +1,7 @@ +
+
{{ 'product.form.visibility'|trans }}
+

{{ 'new_product.form.visibility_for'|trans({ + '%product%': product + }) }}

+ {{ form_widget(visibility) }} +
diff --git a/templates/components/utils/_badge.html.twig b/templates/components/utils/_badge.html.twig new file mode 100644 index 0000000..1690f7c --- /dev/null +++ b/templates/components/utils/_badge.html.twig @@ -0,0 +1,33 @@ +{% if type == "secondary" %} + + + {{ name }} + +{% elseif type == "primary" %} + + + {{ name }} + +{% elseif type == "danger" %} + + + {{ name }} + +{% elseif type == "success" %} + + + {{ name }} + +{% elseif type == "warning" %} + + + {{ name }} + +{% elseif type == "invited member" %} + + + {{ name }} + +{% elseif type.isPublic %} + +{% endif %} diff --git a/templates/components/utils/_notification.html.twig b/templates/components/utils/_notification.html.twig new file mode 100644 index 0000000..bf7b121 --- /dev/null +++ b/templates/components/utils/_notification.html.twig @@ -0,0 +1,21 @@ +
+ +
diff --git a/templates/easy_admin/button/back.html.twig b/templates/easy_admin/button/back.html.twig new file mode 100644 index 0000000..a1fdd22 --- /dev/null +++ b/templates/easy_admin/button/back.html.twig @@ -0,0 +1,2 @@ +{% trans_default_domain 'admin' %} +{{ 'action.back'|trans }} diff --git a/templates/easy_admin/crud/_edit_image_preview.html.twig b/templates/easy_admin/crud/_edit_image_preview.html.twig new file mode 100644 index 0000000..8400bbd --- /dev/null +++ b/templates/easy_admin/crud/_edit_image_preview.html.twig @@ -0,0 +1,14 @@ +{# ## Custom preview ################################################ #} +{% if attribute(ea.entity.instance, form.vars.name) is not empty and form.vars.valid %} +
+ {% set html_id = 'ea-lightbox-menu' %} + + {{ ea.entity.instance.name ?? '' }} + + +
+ {{ ea.entity.instance.name ?? '' }} +
+
+{% endif %} +{# ## Custom preview END ############################################# #} diff --git a/templates/easy_admin/crud/_js_upload_size_validation.html.twig b/templates/easy_admin/crud/_js_upload_size_validation.html.twig new file mode 100644 index 0000000..9c6cc77 --- /dev/null +++ b/templates/easy_admin/crud/_js_upload_size_validation.html.twig @@ -0,0 +1,21 @@ +{# ## Custom JS validation ################################################### #} +{# This is a specific error div for the client side validation #} +{# It works for botth single and multiple uploads #} +{# @todo create a stimulus controller #} +
+ diff --git a/templates/easy_admin/crud/form_theme.html.twig b/templates/easy_admin/crud/form_theme.html.twig new file mode 100755 index 0000000..9466cc2 --- /dev/null +++ b/templates/easy_admin/crud/form_theme.html.twig @@ -0,0 +1,77 @@ +{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #} +{# @see src/Resources/views/crud/form_theme.html.twig #} +{# @see vendor/easycorp/easyadmin-bundle/src/Resources/views/crud/form_theme.html.twig #} + +{# ## Modifictions are only in the include of /easy_admin/* custom templates #} +{# ## All modifictions should be in partial only #} + +{% block ea_fileupload_widget %} + + {# We don't use the existing EA currentFiles variable as it doesn't contains correct values with the cloud storage (prod env) #} + {# because EA tries to create UploadedFile instances from the local path, and it doesn't find anything. #} + {# Instead, we can just get the images path from our entities and use the custom flysystem path helpers #} + + {% set entity = ea.entity.instance %} + {% set current_files = is_images_entity(entity) ? entity.images : [entity.image] %} + {% set current_files = (current_files ?? [])|filter(v => v is not null) %} {# no error with new image #} + +
+
+ {% set placeholder = t('action.choose_file', {}, 'EasyAdminBundle') %} + {% set title = '' %} + {% set files_label = 'files'|trans({}, 'EasyAdminBundle') %} + {% set placeholder = (current_files|length) > 0 ? current_files|length ~ ' ' ~ files_label : placeholder %} +
+ {{ form_widget(form.file, {attr: form.file.vars.attr|merge({placeholder: placeholder, title: title, 'data-files-label': files_label, style: 'display: none'})}) }} + {{ form_label(form.file, placeholder, {label_attr: {class: 'custom-file-label'}}) }} +
+
+ {% if allow_delete %} + + {% endif %} + +
+
+ {% if current_files is not empty and form.vars.valid %} +
+ + + {# If multiple file is not acticated we just have one file #} + {% for file in current_files %} + {# get public URL of resource depending on if it has multiple images associated or not #} + {% set public_url = is_images_entity(entity) ? entity|public_url_image(file) : entity|public_url %} + + + + + {% endfor %} + +
+ {% set html_id = 'ea-lightbox-' ~ loop.index %} + + + {{ file }} + + + +
+ {{ entity.name ?? '' }} +
+ +
+ {{ entity.name ?? '' }} +
+
+ {% endif %} + {% if allow_delete %} +
{{ form_widget(form.delete, {label: false}) }}
+ {% endif %} +
+ {{ form_errors(form.file) }} + + {% include 'easy_admin/crud/_js_upload_size_validation.html.twig' %} +{% endblock %} diff --git a/templates/easy_admin/field/boolean.html.twig b/templates/easy_admin/field/boolean.html.twig new file mode 100644 index 0000000..e724e0f --- /dev/null +++ b/templates/easy_admin/field/boolean.html.twig @@ -0,0 +1,7 @@ +{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #} +{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #} +{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #} +{# @see vendor/easycorp/easyadmin-bundle/src/Resources/views/crud/field/boolean.html.twig #} +{% trans_default_domain 'EasyAdminBundle' %} + + {{ field.value == true ? '✅' : '❌' }} diff --git a/templates/easy_admin/field/boolean_check_only.html.twig b/templates/easy_admin/field/boolean_check_only.html.twig new file mode 100644 index 0000000..f054377 --- /dev/null +++ b/templates/easy_admin/field/boolean_check_only.html.twig @@ -0,0 +1,7 @@ +{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #} +{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #} +{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #} +{# @see vendor/easycorp/easyadmin-bundle/src/Resources/views/crud/field/boolean.html.twig #} +{% trans_default_domain 'EasyAdminBundle' %} + + {{ field.value == true ? '✅' : '' }} diff --git a/templates/easy_admin/field/flysystem_image.html.twig b/templates/easy_admin/field/flysystem_image.html.twig new file mode 100644 index 0000000..fb29771 --- /dev/null +++ b/templates/easy_admin/field/flysystem_image.html.twig @@ -0,0 +1,20 @@ +{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #} +{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #} +{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #} +{% set images = field.formattedValue %} +{% if images is not iterable %} + {% set images = [images] %} +{% endif %} + +{% for image in images %} + {% if image is not empty %} + {% set html_id = 'ea-lightbox-' ~ field.uniqueId ~ '-' ~ loop.index %} + + {{ entity.instance.name ?? '' }} + + +
+ {{ entity.instance.name ?? '' }} +
+ {% endif %} +{% endfor %} diff --git a/templates/easy_admin/field/flysystem_images.html.twig b/templates/easy_admin/field/flysystem_images.html.twig new file mode 100644 index 0000000..89708ae --- /dev/null +++ b/templates/easy_admin/field/flysystem_images.html.twig @@ -0,0 +1,27 @@ +{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #} +{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #} +{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #} + +{# We need the raw value here (an array because the extensions handle the public_url() prefix #} +{% set images = field.value %} +{% if images is not iterable %} + {% set images = [images] %} +{% endif %} + +{# Check if we want to display one image or multiple ones #} +{% if field.customOption('first_image_only') == true %} + {% set images = images|slice(0, 1) %} +{% endif %} + +{% for image in images %} + {% if image is not empty %} + {% set html_id = 'ea-lightbox-' ~ field.uniqueId ~ '-' ~ loop.index %} + + {{ entity.instance.name ?? '' }} + + +
+ {{ entity.instance.name ?? '' }} +
+ {% endif %} +{% endfor %} diff --git a/templates/easy_admin/field/json.html.twig b/templates/easy_admin/field/json.html.twig new file mode 100644 index 0000000..ef424f3 --- /dev/null +++ b/templates/easy_admin/field/json.html.twig @@ -0,0 +1 @@ +
{{ field.getValue()|json_encode(constant('JSON_PRETTY_PRINT')) }}
diff --git a/templates/easy_admin/field/user_email.html.twig b/templates/easy_admin/field/user_email.html.twig new file mode 100644 index 0000000..f0bce18 --- /dev/null +++ b/templates/easy_admin/field/user_email.html.twig @@ -0,0 +1 @@ +{{ field.value.email }} diff --git a/templates/email/admin/group/group_invitation.html.twig b/templates/email/admin/group/group_invitation.html.twig new file mode 100644 index 0000000..0457c2a --- /dev/null +++ b/templates/email/admin/group/group_invitation.html.twig @@ -0,0 +1,19 @@ +{% trans_default_domain 'email' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% set url = url('app_group_show_logged', {id: group.id, slug: group.slug}) %} + +

{{ (i18n_prefix ~ '.hi')|trans }}

+ +

{{ (i18n_prefix ~ '.p1')|trans({'%group%': group.name}) }}

+ +

{{ (i18n_prefix ~ '.p2')|trans }}

+ +

{{ (i18n_prefix ~ '.cta')|trans }}

+ +

{{ (i18n_prefix ~ '.cta2')|trans }}

+ +

{{ url }}

+ +

{{ brand }}

diff --git a/templates/email/admin/new/new_admin.html.twig b/templates/email/admin/new/new_admin.html.twig new file mode 100644 index 0000000..efa37a5 --- /dev/null +++ b/templates/email/admin/new/new_admin.html.twig @@ -0,0 +1,10 @@ +{% trans_default_domain 'email' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +

{{ (i18n_prefix ~ '.p1')|trans }}

+ +

{{ (i18n_prefix ~ '.p2')|trans }}

+

{{ (i18n_prefix ~ '.p3')|trans }}

+ +

{{ brand }}

diff --git a/templates/email/admin/promote_to_admin/promote_to_admin.html.twig b/templates/email/admin/promote_to_admin/promote_to_admin.html.twig new file mode 100644 index 0000000..2e20813 --- /dev/null +++ b/templates/email/admin/promote_to_admin/promote_to_admin.html.twig @@ -0,0 +1,9 @@ +{% trans_default_domain 'email' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +

{{ (i18n_prefix ~ '.p1')|trans }}

+ +

{{ (i18n_prefix ~ '.p2')|trans }}

+ +

{{ brand }}

diff --git a/templates/email/admin/user_group/admin_promotion.html.twig b/templates/email/admin/user_group/admin_promotion.html.twig new file mode 100644 index 0000000..0c0fbd1 --- /dev/null +++ b/templates/email/admin/user_group/admin_promotion.html.twig @@ -0,0 +1,9 @@ +{% trans_default_domain 'email' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +

{{ (i18n_prefix ~ '.h1')|trans }}

+ +

{{ (i18n_prefix ~ '.p1')|trans({'%group%': group.name}) }}

+ +

{{ brand }}

diff --git a/templates/email/admin/user_group/main_admin_promotion.html.twig b/templates/email/admin/user_group/main_admin_promotion.html.twig new file mode 100644 index 0000000..0c0fbd1 --- /dev/null +++ b/templates/email/admin/user_group/main_admin_promotion.html.twig @@ -0,0 +1,9 @@ +{% trans_default_domain 'email' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +

{{ (i18n_prefix ~ '.h1')|trans }}

+ +

{{ (i18n_prefix ~ '.p1')|trans({'%group%': group.name}) }}

+ +

{{ brand }}

diff --git a/templates/email/command/end_membership.html.twig b/templates/email/command/end_membership.html.twig new file mode 100644 index 0000000..0c0fbd1 --- /dev/null +++ b/templates/email/command/end_membership.html.twig @@ -0,0 +1,9 @@ +{% trans_default_domain 'email' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +

{{ (i18n_prefix ~ '.h1')|trans }}

+ +

{{ (i18n_prefix ~ '.p1')|trans({'%group%': group.name}) }}

+ +

{{ brand }}

diff --git a/templates/email/command/notify_membership_expiration.html.twig b/templates/email/command/notify_membership_expiration.html.twig new file mode 100644 index 0000000..5ecdb71 --- /dev/null +++ b/templates/email/command/notify_membership_expiration.html.twig @@ -0,0 +1,9 @@ +{% trans_default_domain 'email' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +

{{ (i18n_prefix ~ '.h1')|trans }}

+ +

{{ (i18n_prefix ~ '.p1')|trans({'%group%': group.name, '%days%': days}) }}

+ +

{{ brand }}

diff --git a/templates/email/command/notify_service_request_end.html.twig b/templates/email/command/notify_service_request_end.html.twig new file mode 100644 index 0000000..b445d6a --- /dev/null +++ b/templates/email/command/notify_service_request_end.html.twig @@ -0,0 +1,9 @@ +{% trans_default_domain 'email' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +

{{ (i18n_prefix ~ '.h1')|trans }}

+ +

{{ (i18n_prefix ~ '.p1')|trans({'%product%': service_request.product.name, '%date%': service_request.endAt|date('format.date'|trans({}, 'date'))}) }}

+ +

{{ brand }}

diff --git a/templates/email/command/notify_service_request_start.html.twig b/templates/email/command/notify_service_request_start.html.twig new file mode 100644 index 0000000..ca97f03 --- /dev/null +++ b/templates/email/command/notify_service_request_start.html.twig @@ -0,0 +1,9 @@ +{% trans_default_domain 'email' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +

{{ (i18n_prefix ~ '.h1')|trans }}

+ +

{{ (i18n_prefix ~ '.p1')|trans({'%product%': service_request.product.name, '%date%': service_request.startAt|date('format.date'|trans({}, 'date'))}) }}

+ +

{{ brand }}

diff --git a/templates/email/security/create_account_step1.html.twig b/templates/email/security/create_account_step1.html.twig new file mode 100644 index 0000000..ea6e461 --- /dev/null +++ b/templates/email/security/create_account_step1.html.twig @@ -0,0 +1,22 @@ +{% trans_default_domain 'email' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% set url = url('security_account_create_step2', {token: user.confirmationToken}) %} + +

{{ (i18n_prefix ~ '.hi')|trans }}

+ +{% if group is defined and group is not null %} +

{{ (i18n_prefix ~ '.invitation.p1')|trans({'%group%': group.name}) }}

+

{{ (i18n_prefix ~ '.invitation.p2')|trans }}

+{% endif %} + +

{{ (i18n_prefix ~ '.p1')|trans }}

+ +

{{ (i18n_prefix ~ '.cta')|trans }}

+ +

{{ (i18n_prefix ~ '.cta2')|trans }}

+ +

{{ url }}

+ +

{{ brand }}

diff --git a/templates/email/security/lost_password.html.twig b/templates/email/security/lost_password.html.twig new file mode 100644 index 0000000..254fde7 --- /dev/null +++ b/templates/email/security/lost_password.html.twig @@ -0,0 +1,15 @@ +{% trans_default_domain 'email' %} + +{% set url = url('security_reset_password', {token: user.lostPasswordToken}) %} + +

Bonjour !

+ +

Cliquez sur le lien suivant pour réinitialiser votre mot de passe

+ +

Réinitialiser mon mot de passe

+ +

Ou copiez / coller le lien ci-dessous dans votre navigateur

+ +

{{ url }}

+ +

{{ brand }}

diff --git a/templates/email/service_request/accepted.html.twig b/templates/email/service_request/accepted.html.twig new file mode 100644 index 0000000..04660ee --- /dev/null +++ b/templates/email/service_request/accepted.html.twig @@ -0,0 +1,20 @@ +{% trans_default_domain 'email' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% set url = url('app_user_conversation_list', {id: service_request.id}) %} + +

{{ (i18n_prefix ~ '.hi')|trans }}

+ +

{{ (i18n_prefix ~ '.p1')|trans({ + '%date%': service_request.endAt|date('format.date'|trans({}, 'date')), + '%product_name%': service_request.product.name, + '%owner_display_name%': service_request.owner.displayName, + }) +}}

+ +

{{ (i18n_prefix ~ '.p2')|trans }}

+ +

{{ (i18n_prefix ~ '.cta')|trans }}

+ +

{{ brand }}

diff --git a/templates/email/service_request/confirmed.html.twig b/templates/email/service_request/confirmed.html.twig new file mode 100644 index 0000000..3514acb --- /dev/null +++ b/templates/email/service_request/confirmed.html.twig @@ -0,0 +1,20 @@ +{% trans_default_domain 'email' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% set url = url('app_user_conversation_list', {id: service_request.id}) %} + +

{{ (i18n_prefix ~ '.hi')|trans }}

+ +

{{ (i18n_prefix ~ '.p1')|trans({ + '%date%': service_request.endAt|date('format.date'|trans({}, 'date')), + '%product_name%': service_request.product.name, + '%recipient_display_name%': service_request.recipient.displayName, + }) +}}

+ +

{{ (i18n_prefix ~ '.p2')|trans }}

+ +

{{ (i18n_prefix ~ '.cta')|trans }}

+ +

{{ brand }}

diff --git a/templates/email/service_request/message/new.html.twig b/templates/email/service_request/message/new.html.twig new file mode 100644 index 0000000..868a33f --- /dev/null +++ b/templates/email/service_request/message/new.html.twig @@ -0,0 +1,19 @@ +{% trans_default_domain 'email' %} + +{% set url = url('app_user_conversation_list', {id: service_request.id}) %} + +

Bonjour !

+ +

+ Un nouveau message concernant la demande de service du + {{ service_request.endAt|date('format.date'|trans({}, 'date')) }} ({{ service_request.product.name }}) + a été envoyé par {{ message.sender.displayName }}. +

+ +

+ Message : {{ message.message }} +

+ +

Accéder à la conversation

+ +

{{ brand }}

diff --git a/templates/email/service_request/modified_by.html.twig b/templates/email/service_request/modified_by.html.twig new file mode 100644 index 0000000..f655c7e --- /dev/null +++ b/templates/email/service_request/modified_by.html.twig @@ -0,0 +1,24 @@ +{% trans_default_domain 'email' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% set url = url('app_user_conversation_list', {id: service_request.id}) %} + +

{{ (i18n_prefix ~ '.hi')|trans }}

+ +

{{ (i18n_prefix ~ '.p1')|trans({ + '%date%': service_request.createdAt|date('format.date'|trans({}, 'date')), + '%product_name%': service_request.product.name, + '%modified_by%': modified_by, + }) }}

+ +

{{ (i18n_prefix ~ '.p2')|trans({ + '%startAt%': service_request.startAt|date('format.date'|trans({}, 'date')), + '%endAt%': service_request.endAt|date('format.date'|trans({}, 'date')), + }) }}

+ +

{{ (i18n_prefix ~ '.p3')|trans }}

+ +

{{ (i18n_prefix ~ '.cta')|trans }}

+ +

{{ brand }}

diff --git a/templates/email/service_request/new.html.twig b/templates/email/service_request/new.html.twig new file mode 100644 index 0000000..90df3da --- /dev/null +++ b/templates/email/service_request/new.html.twig @@ -0,0 +1,17 @@ +{% trans_default_domain 'email' %} + +{# @todo : use translations #} + +{% set url = url('app_user_my_lendings') %} + +

Bonjour !

+ +

+ Une nouvelle demande d'emprunt du {{ service_request.startAt|date('d/m/Y') }} au + {{ service_request.endAt|date('d/m/Y') }} concernant votre objet + {{ service_request.product.name }} a été créé par {{ service_request.recipient.displayName }}. +

+ +

Voir la demande

+ +

{{ brand }}

diff --git a/templates/email/service_request/refused.html.twig b/templates/email/service_request/refused.html.twig new file mode 100644 index 0000000..57d6357 --- /dev/null +++ b/templates/email/service_request/refused.html.twig @@ -0,0 +1,18 @@ +{% trans_default_domain 'email' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% set url = url('app_user_conversation_list', {id: service_request.id}) %} + +

{{ (i18n_prefix ~ '.hi')|trans }}

+ +

{{ (i18n_prefix ~ '.p1')|trans({ + '%date%': service_request.endAt|date('format.date'|trans({}, 'date')), + '%product_name%': service_request.product.name, + '%actor%': actor.displayName ?? '', + }) +}}

+ +

{{ (i18n_prefix ~ '.cta')|trans }}

+ +

{{ brand }}

diff --git a/templates/index.html.twig b/templates/index.html.twig new file mode 100644 index 0000000..a17f367 --- /dev/null +++ b/templates/index.html.twig @@ -0,0 +1,24 @@ +{% extends 'layout/base.html.twig' %} + +{% block link %}{% endblock %} + +{% block body %} + +{% endblock %} diff --git a/templates/layout/base.html.twig b/templates/layout/base.html.twig new file mode 100644 index 0000000..2518dd1 --- /dev/null +++ b/templates/layout/base.html.twig @@ -0,0 +1,66 @@ + + + + + {% block title %}{{ brand }}{% endblock %} + + + + + + {% block stylesheets %} + {{ encore_entry_link_tags('app') }} + {% endblock %} + + {% block javascripts %} + {{ encore_entry_script_tags('app') }} + {% endblock %} + + +{% include 'components/utils/_notification.html.twig' with { + name: 'error', + text: 'templates.layout.base.notification.error'|trans, + type: 'danger' +} %} + +{% block header %} +
+ {{ render(controller('App\\Controller\\Menu\\MenuController::menu', {q: app.request.query.get('q')})) }} +
+{% endblock %} +
+
+
+ {% for label, messages in app.flashes %} + {% for message in messages %} + + {% endfor %} + {% endfor %} +
+
+ {% block body %}{% endblock %} + {% if app.request.get('_route') != 'home' %} + {% block link %} +
+
+ Retour +
+
+ {% endblock %} + {% endif %} +
+{% block footer %} +
+ {% block funding %}{% endblock %} + {{ render(controller( + 'App\\Controller\\Menu\\MenuController::footerItems' + )) }} + + {% endblock %} +
+ + diff --git a/templates/layout/connection.html.twig b/templates/layout/connection.html.twig new file mode 100644 index 0000000..0476577 --- /dev/null +++ b/templates/layout/connection.html.twig @@ -0,0 +1,40 @@ +{% extends 'layout/base.html.twig' %} + +{% block body %} +
+ {% block page_title %} + {% include 'components/layout/_title_3.html.twig' with { + name + } %} + {% endblock %} + + {% block text %} +
+
+

{{ info|raw }}

+
+
+ {% endblock %} + + {% block form %} +
+
+ {% block form_element %}{% endblock %} + {% block error %} + {% if error %} + + {% endif %} + {% endblock %} +
+
+ {% endblock %} + {% block alert %}{% endblock %} +
+{% endblock %} + +{% block link %} + +{% endblock %} diff --git a/templates/pages/account/address/step1.html.twig b/templates/pages/account/address/step1.html.twig new file mode 100644 index 0000000..9188c9b --- /dev/null +++ b/templates/pages/account/address/step1.html.twig @@ -0,0 +1,35 @@ +{% extends 'layout/base.html.twig' %} + +{% form_theme form 'bootstrap_5_layout.html.twig' %} + +{% block body %} + {% include 'components/layout/_title_3.html.twig' with { + name: 'address.step1_action.title'|trans + } %} + {% include 'components/layout/_text.html.twig' with { + text: 'address.step1_action.plaintext'|trans + } %} + +
+
+
+ {{ form_start(form, { + attr: { + novalidate: true + } + }) }} + {{ form_row(form.address) }} + {{ form_row(form.addressSupplement) }} + {{ form_row(form.postalCode) }} + {{ form_row(form.locality) }} + {{ form_row(form.country) }} +
+ {{ form_widget(form.submit) }} +
+ {{ form_end(form) }} +
+
+
+ {% block step2 %}{% endblock %} +{% endblock %} +{% block link %}{% endblock %} diff --git a/templates/pages/account/address/step2.html.twig b/templates/pages/account/address/step2.html.twig new file mode 100644 index 0000000..c2f9e7e --- /dev/null +++ b/templates/pages/account/address/step2.html.twig @@ -0,0 +1,32 @@ +{% extends 'layout/base.html.twig' %} + +{% form_theme form 'components/form/_address_form_theme.html.twig' %} + +{% block body %} +
+
+
+

{{ 'address.step2_action.title'|trans }}

+ +

{{ 'address.step2_action.user_input'|trans }}

+ +
{{ address.fullAddress }}
+ +

{{ 'address.step2_action.confirm_title'|trans }}

+ +

{{ 'address.step2_action.confirm_intro'|trans }}

+ {{ form_start(form) }} + {{ form_row(form.addresses) }} +
+ {{ form_widget(form.submit) }} +
+ {{ form_end(form) }} +
+
+
+{% endblock %} + +{% block link %}{% endblock %} diff --git a/templates/pages/account/conversation.html.twig b/templates/pages/account/conversation.html.twig new file mode 100644 index 0000000..5e2da8a --- /dev/null +++ b/templates/pages/account/conversation.html.twig @@ -0,0 +1,204 @@ +{% extends 'layout/base.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} +{% set product = service_request.getProduct() %} +{% set category = product.getCategory() %} +{% set is_owner = service_request.owner == app.user %} +{% set contact = is_owner ? service_request.recipient : service_request.owner %} +{% set csrf_token = csrf_token('transition') %} +{% set start_at = service_request.startAt|date('d/m/Y') %} +{% set end_at = service_request.endAt|date('d/m/Y') %} +{% set service_request_id = service_request.id %} + +{% block container %}container-fluid{% endblock %} + +{% block body %} + {% if app.environment == 'dev' %} +
+
+ debug +
+                i18n_prefix: {{ i18n_prefix }}
+
+                Current service request status: {{ service_request.status.value }}
+
+                canAccept : {{ workflow_can(service_request, 'accept') ? '✅' : '❌' }}
+                canConfirm : {{ workflow_can(service_request, 'confirm') ? '✅' : '❌' }}
+                canRefuse : {{ workflow_can(service_request, 'refuse') ? '✅' : '❌' }}
+
+                service_request.isOwner(app.user): {{ service_request.isOwner(app.user) ? '✅' : '❌' }}
+                service_request.isRecipient(app.user): {{ service_request.isRecipient(app.user) ? '✅' : '❌' }}
+
+                status.isNew : {{ service_request.status.isNew ? '✅' : '❌' }}
+                status.isToConfirm : {{ service_request.status.isToConfirm ? '✅' : '❌' }}
+                status.isConfirmed : {{ service_request.status.isConfirmed ? '✅' : '❌' }}
+                status.isRefused : {{ service_request.status.isRefused ? '✅' : '❌' }}
+                status.isFinished : {{ service_request.status.isFinished ? '✅' : '❌' }}
+
+                csrf_token: {{ csrf_token }}
+                
+
+
+ {% endif %} + {# Edit dates modal #} + {% if workflow_can(service_request, 'modifyOwner') or workflow_can(service_request, 'modifyRecipient') %} + + {% endif %} + +
+
+
+
+ + + + {% include 'components/conversation/_avatar.html.twig' with { + contact, + name: is_owner ? contact.displayName : service_request.owner.displayName + } %} +
+
+ + {% include 'components/conversation/_product-info.html.twig' with { + product, + category, + rowClass: 'p-3 bg-secondary-subtle mt-lg-2' + } %} + + {% include 'components/conversation/_calendar.html.twig' with { + start_at, + end_at, + workflowCanModifyOwner: workflow_can(service_request, 'modifyOwner'), + workflowCanModifyRecipient: workflow_can(service_request, 'modifyRecipient'), + } %} +
+
+ {% include 'components/conversation/_badge.html.twig' with { + status: service_request.status + } %} +
+
+ {% if service_request.status.name != 'FINISHED' %} +
+ {% include 'components/conversation/_action.html.twig' with { + accept: workflow_can(service_request, 'accept'), + confirm: workflow_can(service_request, 'confirm'), + finalize: workflow_can(service_request, 'finalize'), + refuse: workflow_can(service_request, 'refuse') + } %} +
+ + {% else %} + + {% endif %} +
+
+ {% include 'components/conversation/_chat.html.twig' with { + messages: service_request.messages + } %} +
+
+ +
+
+
+ {{ form_start(form) }} +
+
+ {{ form_widget(form.message, { + attr: { + placeholder: 'templates.pages.account.conversation.textarea.placeholder'|trans, + class: 'conversation-textarea', + }}) }} +
+
+ {{ form_widget(form.submit) }} +
+ {{ form_widget(form._token) }} +
+ {{ form_end(form) }} +
+ {% include 'components/conversation/_action.html.twig' with { + accept: workflow_can(service_request, 'accept'), + confirm: workflow_can(service_request, 'confirm'), + finalize: workflow_can(service_request, 'finalize'), + refuse: workflow_can(service_request, 'refuse') + } %} +
+
+
+
+ + {% include 'components/conversation/_modal-confirmation.html.twig' with { + start_at, + end_at, + product, + category, + contact, + name: is_owner ? contact.displayName : service_request.owner.displayName + } %} + +{% endblock %} + +{% block link %}{% endblock %} + +{% block footer %}{% endblock %} diff --git a/templates/pages/account/index.html.twig b/templates/pages/account/index.html.twig new file mode 100644 index 0000000..15bf1ae --- /dev/null +++ b/templates/pages/account/index.html.twig @@ -0,0 +1,199 @@ +{% extends 'layout/base.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% set vacation_mode_link = app.user.vacationMode ? (i18n_prefix ~ '.vacation_mode_desactivate')|trans : (i18n_prefix ~ '.vacation_mode_activate')|trans %} +{% set vacation_mode_icon = app.user.vacationMode ? 'fa-solid fa-pause' : 'fa-solid fa-play' %} +{% set has_admin_access = is_granted('ROLE_ADMIN') or is_granted('ROLE_GROUP_ADMIN') %} +{% set show_my_groups = has_admin_access %} + +{% if canCreateGroup %} + {% set create_group_label = (i18n_prefix ~ '.create_group')|trans %} + {% set create_group_icon = null %} +{% else %} + {% set create_group_label = (i18n_prefix ~ '.creation_request')|trans %} + {% set create_group_icon = 'fa-regular fa-envelope' %} +{% endif %} + +{% set my_account_links = [ + { + section: 'Messagerie', + links: [ + { + name: 'Mes emprunts', + link: 'app_user_my_loans', + icon: null, + notification: userHasNewLoanMessage + }, + { + name: 'Mes prêts', + link: 'app_user_my_lendings', + icon: null, + notification: userHasNewLendingMessage + }, + ], + icon: 'bi bi-chat-left-text' + }, + { + section: 'Mes objets', + links: [ + { + name: 'Voir mes objets', + link: 'app_user_objects', + icon: null + }, + { + name: 'Créer un objet', + link: 'app_object_new', + icon: null, + needAddress: app.user.address is null ? true : false + }, + ], + icon: 'fa-solid fa-shop' + }, + { + section: 'Mes services', + links: [ + { + name: 'Voir mes services', + link: 'app_user_services', + icon: null + }, + { + name: 'Créer un service', + link: 'app_service_new', + icon: null, + needAddress: app.user.address is null ? true : false + }, + ], + icon: 'fa-solid fa-shop' + }, + { + section: 'Mes groupes', + links: [ + { + name: 'Voir mes groupes', + link: 'app_user_groups', + icon: null + }, + { + name: 'Administrer mes groupes', + link: 'admin', + show: show_my_groups, + icon: 'bi bi-box-arrow-up-right' + }, + { + name: create_group_label, + link: 'app_group_create', + icon: create_group_icon, + canCreateGroup: canCreateGroup, + }, + ], + icon: 'fa-solid fa-user-group' + }, + { + section: 'Compte', + links: [ + { + name: 'Mon adresse', + link: 'user_address_step1', + icon: null + }, + { + name: 'Modifier mon profil', + link: 'app_user_edit_profile', + icon: null + }, + { + name: 'Changer mon adresse e-mail', + link: 'app_user_change_login', + icon: null + }, + { + name: 'Changer mon mot de passe', + link: 'app_user_change_password', + icon: null + }, + { + name: vacation_mode_link, + link: 'user_toggle_vacation_mode', + icon: vacation_mode_icon + }, + ], + icon: 'fa-solid fa-user fa-xl text-white' + }, +] %} + +{% block app_name %}{% endblock %} + +{% block body %} +
+ {% include 'components/layout/_title_3.html.twig' with { + name: (i18n_prefix ~ '.my_account')|trans + } %} +
+ {% for my_account_link in my_account_links %} +
+ +
+ {% endfor %} +
+ +
+{% endblock %} + +{% block link %}{% endblock %} + +{# TODO: Traduction + Link + icons #} diff --git a/templates/pages/account/lendings/list.html.twig b/templates/pages/account/lendings/list.html.twig new file mode 100644 index 0000000..ae995bc --- /dev/null +++ b/templates/pages/account/lendings/list.html.twig @@ -0,0 +1,30 @@ +{% extends 'layout/base.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% block body %} + {% include 'components/layout/_title_3.html.twig' with { + name: (i18n_prefix ~ '.title')|trans + } %} +
+ {% include 'components/layout/_multiple_searchbar.html.twig' with { + label: (i18n_prefix ~ '.filterBy')|trans, + placeholder: (i18n_prefix ~ '.search')|trans, + margin: 'mx-auto', + form: form, + } %} +
+ {% if pagination.count > 0 %} + {% include 'components/list/_conversations.html.twig' with { + pagination, + isMyLoans: false + } %} + {% else %} +

{{ (i18n_prefix ~ '.no_result')|trans }}

+ {% endif %} +
+ {% include 'components/layout/_pagination.html.twig' with {pagination} %} +
+{% endblock %} + +{% block link %}{% endblock %} diff --git a/templates/pages/account/loans/list.html.twig b/templates/pages/account/loans/list.html.twig new file mode 100644 index 0000000..2d86003 --- /dev/null +++ b/templates/pages/account/loans/list.html.twig @@ -0,0 +1,29 @@ +{% extends 'layout/base.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% block body %} + {% include 'components/layout/_title_3.html.twig' with { + name: (i18n_prefix ~ '.title')|trans + } %} +
+ {% include 'components/layout/_multiple_searchbar.html.twig' with { + label: (i18n_prefix ~ '.filterBy')|trans, + placeholder: (i18n_prefix ~ '.search')|trans, + margin: 'mx-auto' + } %} +
+ {% if pagination.count > 0 %} + {% include 'components/list/_conversations.html.twig' with { + pagination, + isMyLoans: true + } %} + {% else %} +

{{ (i18n_prefix ~ '.no_result')|trans }}

+ {% endif %} +
+ {% include 'components/layout/_pagination.html.twig' with {pagination} %} +
+{% endblock %} + +{% block link %}{% endblock %} diff --git a/templates/pages/account/loans/new.html.twig b/templates/pages/account/loans/new.html.twig new file mode 100644 index 0000000..0a47fa6 --- /dev/null +++ b/templates/pages/account/loans/new.html.twig @@ -0,0 +1,63 @@ +{% extends 'layout/base.html.twig' %} + +{% form_theme form 'bootstrap_5_layout.html.twig' %} + +{% block body %} + {% include 'components/layout/_back_to_link.html.twig' with { + name: 'templates.pages.account.loans.new.back_to_article'|trans, + link: { + name: 'app_product_show', + params: { + 'slug': product.slug, + 'id': product.id + } + } + } %} + + {% include 'components/layout/_title_3.html.twig' with { + name: 'templates.pages.account.loans.new.title'|trans + } %} + + {{ form_start(form, { + attr: { + novalidate: true, + } + }) }} +
+
+ {% if product.images is not empty %} + {{ product.name }} + {% elseif product.category.image is not empty %} + {{ product.category.name }} + {% else %} + {{ product.category.name }} + {% endif %} +
+ {{ product.name }} + + {% if product.category.parent is not null %} + {{ product.category.parent }} / {{ product.category }} + {% else %} + {{ product.category }} + {% endif %} + +
+
+
+ {% include 'components/product/_calendar.html.twig' with { + title: 'product.new.calendar_title'|trans, + form: form, + actionNeeded: false + } %} +
+
+ {% include 'components/product/_write_message.html.twig' with { + form: form + } %} +
+
+ {{ form_end(form) }} +{% endblock %} + +{% block link %}{% endblock %} diff --git a/templates/pages/account/product/list.html.twig b/templates/pages/account/product/list.html.twig new file mode 100644 index 0000000..eb45cbd --- /dev/null +++ b/templates/pages/account/product/list.html.twig @@ -0,0 +1,75 @@ +{% extends 'layout/base.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% block body %} +
+ + {% include 'components/utils/_notification.html.twig' with { + name: 'activeStatusSuccess', + text: (i18n_prefix ~ '.notification.active_success')|trans, + type: 'success' + } %} + + {% include 'components/utils/_notification.html.twig' with { + name: 'pausedStatusSuccess', + text: (i18n_prefix ~ '.notification.paused_success')|trans, + type: 'success' + } %} + + {% include 'components/layout/_title_3.html.twig' with { + name: pagination.route == 'app_user_objects' ? (i18n_prefix ~ '.object.title')|trans : (i18n_prefix ~ '.service.title')|trans + } %} + +
+
+ {% include 'components/form/_select.html.twig' with { + label: (i18n_prefix ~ '.service.filter_by')|trans, + name: (i18n_prefix ~ '.category')|trans, + } %} +
+
+ {% if pagination.count > 0 %} + {% include 'components/list/_products.html.twig' with { + products: pagination, + route: pagination.route + } %} + {% else %} +

+ {% if pagination.route == 'app_user_objects' %} + {{ (i18n_prefix ~ '.object.no_result')|trans }} + {% else %} + {{ (i18n_prefix ~ '.service.no_result')|trans }} + {% endif %} +

+ {% endif %} + {% include 'components/layout/_pagination.html.twig' with {pagination} %} + + {% if app.user.address is null %} + {% include 'components/product/_modal.html.twig' with { + button: (i18n_prefix ~ '.no-address')|trans, + title: (i18n_prefix ~ '.no-address-title')|trans, + message: (i18n_prefix ~ '.no-address-message')|trans({ + '%product%': pagination.route == 'app_user_objects' ? 'objet' : 'service' + }), + action: (i18n_prefix ~ '.no-address-add')|trans + } %} + {% else %} +
+ {% if pagination.route == 'app_user_objects' %} + + {{ (i18n_prefix ~ '.object.new')|trans }} + + {% elseif pagination.route == 'app_user_services' %} + + {{ (i18n_prefix ~ '.service.new')|trans }} + + {% endif %} +
+ {% endif %} +
+{% endblock %} + +{% block link %}{% endblock %} diff --git a/templates/pages/group/create.html.twig b/templates/pages/group/create.html.twig new file mode 100644 index 0000000..11ee476 --- /dev/null +++ b/templates/pages/group/create.html.twig @@ -0,0 +1,30 @@ +{% extends 'layout/base.html.twig' %} + +{% form_theme form 'bootstrap_5_layout.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + + +{% block body %} +
+ {% include 'components/layout/_title_3.html.twig' with { + rowClass: 'mt-4', + name: (i18n_prefix ~ '.title')|trans + } %} +
+
+ {{ form_start(form) }} + {{ form_row(form.name) }} + {{ form_row(form.type) }} + {{ form_row(form.membership) }} + {% if form.parent is defined and form.parent is not null %} + {{ form_row(form.parent) }} + {% endif %} + {{ form_widget(form.submit) }} + {{ form_end(form) }} +
+
+
+{% endblock %} + +{% block link %}{% endblock %} diff --git a/templates/pages/group/list.html.twig b/templates/pages/group/list.html.twig new file mode 100644 index 0000000..03cd698 --- /dev/null +++ b/templates/pages/group/list.html.twig @@ -0,0 +1,36 @@ +{% extends 'layout/base.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% block container %}container-fluid{% endblock %} + +{% block body %} + {% include 'components/layout/_title_3.html.twig' with { + name: (i18n_prefix ~ '.title')|trans + } %} +
+ {% include 'components/layout/_searchbar.html.twig' with { + col: 'col col-lg-5', + placeholder: (i18n_prefix ~ '.placeholder')|trans, + } %} +
+
+
+
    + {% for group in pagination %} +
  • + {% include 'components/group/_card.html.twig' with { + group, + link: path('app_group_show', {id: group.id, slug: group.slug}) + } %} +
  • + {% endfor %} +
+
+ {% include 'components/layout/_pagination.html.twig' with {pagination} %} +
+
+
+{% endblock %} + +{% block link %}{% endblock %} diff --git a/templates/pages/group/members.html.twig b/templates/pages/group/members.html.twig new file mode 100644 index 0000000..cc282ec --- /dev/null +++ b/templates/pages/group/members.html.twig @@ -0,0 +1,42 @@ +{% extends 'layout/base.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% block body %} + {% include 'components/layout/_title_3.html.twig' with { + name: (i18n_prefix ~ '.title')|trans + } %} +
+ {% include 'components/layout/_searchbar.html.twig' with { + margin: 'my-3 my-lg-0 me-lg-3' + } %} +
+
+ {% if pagination is empty %} +

{{ (i18n_prefix ~ '.no_result')|trans }}

+ {% else %} + {% for member in pagination %} + {% include 'components/group/_member_card.html.twig' with { + user: member.user, + category: member.user.category, + isInvited: member.membership.isInvited, + group, + } %} + {% endfor %} + {% include 'components/layout/_pagination.html.twig' with {pagination} %} + {% if is_granted('IS_AUTHENTICATED') and app.user.isGroupAdmin(group) %} + + {% endif %} + {% endif %} +
+{% endblock %} + +{% block link %}{% endblock %} diff --git a/templates/pages/group/show.html.twig b/templates/pages/group/show.html.twig new file mode 100644 index 0000000..024e604 --- /dev/null +++ b/templates/pages/group/show.html.twig @@ -0,0 +1,61 @@ +{% extends 'layout/base.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{# User logged? #} +{% set logged = is_granted('IS_AUTHENTICATED_REMEMBERED') %} + +{# Is the group public? #} +{% set is_public = group.type.isPublic %} + +{# Get the current status of the user for the current group #} +{% set group_membership = logged ? app.user.getGroupMembership(group) : null %} +{% set has_group_membership = group_membership != null %} + +{# Can we display the show members button? #} +{% set display_show_members_button = is_public or (has_group_membership and group_membership.membership.isConfirmed) %} + +{% block body %} +
+ {% include 'components/layout/_title_3.html.twig' with { + name: group.name + } %} + +
+ {% set pill_bg = group.type.isPublic() ? 'text-primary text-bg-blue-custom' : 'text-bg-secondary' %} + + {{ group.type.value|trans }} + +
+ + {% include 'components/layout/_text.html.twig' with { + text: group.description + } %} + + {# 1. logged but not member and no pending invitaton #} + {% if logged and not has_group_membership %} + {% include 'pages/group/show/_logged_without_link.html.twig' %} + + {# 2. logged and has a link with the group #} + {% elseif logged and has_group_membership %} + {% include 'pages/group/show/_logged_with_link.html.twig' %} + + {# 3. not logged, display the link to login and come back to this page #} + {% else %} +

+ {{ (i18n_prefix ~ '.login_text')|trans }} +

+ {% endif %} + + {% if display_show_members_button %} + + {% endif %} +
+{% endblock %} +{% block link %}{% endblock %} diff --git a/templates/pages/group/show/_logged_with_link.html.twig b/templates/pages/group/show/_logged_with_link.html.twig new file mode 100644 index 0000000..a6eefbc --- /dev/null +++ b/templates/pages/group/show/_logged_with_link.html.twig @@ -0,0 +1,58 @@ +{# 2.1. with a pending invitation #} +{% if group_membership.membership.isInvited %} + + {# 2.1.1 and paying offers #} + {% if not group.activeOffers.isEmpty %} + {% include 'components/group/_first_offer.html.twig' with {group} %} + {% include 'components/group/_modal_offers.html.twig' with { + offers: group.activeOffers + } %} + + {# 2.1.2 otherwise it is a free group #} + {% else %} +
+
+ +
+ +
+
+
+ {% endif %} + +{# 2.2 already member (or group admin) #} +{% else %} +

+ {{ (i18n_prefix ~ '.already_member')|trans }} + {% if group_membership.endAt is not null %} + {{ (i18n_prefix ~ '.membership_valid_until')|trans({'%endAt%': group_membership.endAt|date('format.date'|trans({}, 'date'))}) }} + {% endif %} +

+ + {% if group_membership.membership.isAdmin %} + + {% endif %} + +
+ +
+ {% include 'components/group/_modal.html.twig' with { + title: (i18n_prefix ~ '.form.quit_group.submit-modal')|trans, + message: (i18n_prefix ~ '.form.quit_group.submit-modal-message')|trans + } %} +{% endif %} diff --git a/templates/pages/group/show/_logged_without_link.html.twig b/templates/pages/group/show/_logged_without_link.html.twig new file mode 100644 index 0000000..8a6922e --- /dev/null +++ b/templates/pages/group/show/_logged_without_link.html.twig @@ -0,0 +1,30 @@ +
+ {# 1.1 group is free and public, we can join it #} + {% if is_public and group.membership.isFree %} +
+ +
+ +
+
+ + {# 1.2 group is not free, we have to pay, display the offers #} + {% elseif group.hasActiveOffers %} + + {% include 'components/group/_first_offer.html.twig' with {group} %} + {% include 'components/group/_modal_offers.html.twig' with { + offers: group.activeOffers + } %} + + {# 1.3 group is not correclty configured, it doesn't have any offer configured yet. #} + {% else %} +

{{ (i18n_prefix ~ '.no_offer')|trans }}

+ {% endif %} +
diff --git a/templates/pages/login.html.twig b/templates/pages/login.html.twig new file mode 100644 index 0000000..886aa00 --- /dev/null +++ b/templates/pages/login.html.twig @@ -0,0 +1,66 @@ +{% extends 'layout/connection.html.twig' %} + +{% trans_default_domain 'security' %} + +{% block page_title %} + {% with {name: 'login_action.title'|trans} %} + {{ parent() }} + {% endwith %} +{% endblock %} + +{% block text %}{% endblock %} + +{% block form_element %} +
+
+
+ +
+ +
+ + {% include 'components/form/_password_visibility.html.twig' with {error} %} + + + + + +
+ +
+
+{% endblock %} + +{% block link %} + {% with { + name: 'login_action.create_account.button'|trans, + link: path('security_account_create_step1') + } %} + {{ parent() }} + {% endwith %} +{% endblock %} diff --git a/templates/pages/password/lost.html.twig b/templates/pages/password/lost.html.twig new file mode 100644 index 0000000..e3a0e34 --- /dev/null +++ b/templates/pages/password/lost.html.twig @@ -0,0 +1,45 @@ +{% extends 'layout/connection.html.twig' %} + +{% form_theme form 'bootstrap_5_layout.html.twig' %} + +{% trans_default_domain 'security' %} + +{% block error %}{% endblock %} + +{% block icon %} + {{ parent() }} +{% endblock %} + +{% block page_title %} + {% with {name: 'lost_password_action.title'|trans} %} + {{ parent() }} + {% endwith %} +{% endblock %} + +{% block text %} + {% with {info: 'lost_password_action.info'|trans} %} + {{ parent() }} + {% endwith %} +{% endblock %} + +{% block form_element %} + {{ form_start(form, { + attr: { + novalidate: true + } + }) }} + {{ form_row(form.email) }} +
+ {{ form_widget(form.submit) }} +
+ {{ form_end(form) }} +{% endblock %} + +{% block link %} + {% with { + name: 'lost_password_action.login.link'|trans, + link: path('app_login') + } %} + {{ parent() }} + {% endwith %} +{% endblock %} diff --git a/templates/pages/password/reset.html.twig b/templates/pages/password/reset.html.twig new file mode 100644 index 0000000..f72e6f9 --- /dev/null +++ b/templates/pages/password/reset.html.twig @@ -0,0 +1,44 @@ +{% extends 'layout/connection.html.twig' %} + +{% form_theme form 'bootstrap_5_layout.html.twig' %} + +{% trans_default_domain 'security' %} + +{% block error %}{% endblock %} + +{% block icon %} + {{ parent() }} +{% endblock %} + +{% block page_title %} + {% with {name: 'reset_password_action.title'|trans} %} + {{ parent() }} + {% endwith %} +{% endblock %} + +{% block text %} + {% with {info: 'reset_password_action.info'|trans} %} + {{ parent() }} + {% endwith %} +{% endblock %} + +{% block form_element %} + {{ form_start(form, { + attr: { + novalidate: true + } + }) }} + {% include 'components/form/_password_visibility.html.twig' with { + form: form.password.first, + } %} + {% include 'components/form/_password_visibility.html.twig' with { + form: form.password.second + } %} + +
+ {{ form_widget(form.submit) }} +
+ {{ form_end(form) }} +{% endblock %} + +{% block link %}{% endblock %} diff --git a/templates/pages/product/edit_object.html.twig b/templates/pages/product/edit_object.html.twig new file mode 100644 index 0000000..27d54fa --- /dev/null +++ b/templates/pages/product/edit_object.html.twig @@ -0,0 +1,37 @@ +{% extends 'layout/base.html.twig' %} + +{% form_theme form 'bootstrap_5_layout.html.twig' %} + +{% block body %} +
+ {% include 'components/layout/_title_3.html.twig' with { + name: 'edit_object.form.title'|trans + } %} + {% if info is defined and not null %} + {% include 'components/layout/_text.html.twig' with { + text: info + } %} + {% endif %} +
+
+ {{ form_start(form) }} + {% include 'components/product/_object_fields.html.twig' %} + {% if product.images is not empty %} +
+ {% for image in product.images %} + {% include 'components/form/_photo_preview.html.twig' with { + image, + entity: product, + } %} + {% endfor %} +
+ {% endif %} + {% include 'components/product/_address_form.html.twig' %} +
+ {{ form_widget(form.submit) }} +
+ {{ form_end(form) }} +
+
+
+{% endblock %} diff --git a/templates/pages/product/edit_service.html.twig b/templates/pages/product/edit_service.html.twig new file mode 100644 index 0000000..377e247 --- /dev/null +++ b/templates/pages/product/edit_service.html.twig @@ -0,0 +1,41 @@ +{% extends 'layout/base.html.twig' %} + +{% form_theme form 'bootstrap_5_layout.html.twig' %} + +{% block body %} +
+ {% include 'components/layout/_title_3.html.twig' with { + name: 'edit_service.form.title'|trans + } %} + {% if info is defined and not null %} + {% include 'components/layout/_text.html.twig' with { + text: info + } %} + {% endif %} +
+
+ {{ form_start(form, { + attr: { + novalidate: true, + } + }) }} + {% include 'components/product/_service_fields.html.twig' %} + {% if product.images is not empty %} +
+ {% for image in product.images %} + {% include 'components/form/_photo_preview.html.twig' with { + image, + entity: product, + } %} + {% endfor %} +
+ {% endif %} + {% include 'components/product/_address_form.html.twig' %} + {% include 'components/product/service_request/_submit.html.twig' with { + submit: form.submit + } %} + {{ form_end(form) }} +
+
+
+{% endblock %} diff --git a/templates/pages/product/list.html.twig b/templates/pages/product/list.html.twig new file mode 100644 index 0000000..44971f4 --- /dev/null +++ b/templates/pages/product/list.html.twig @@ -0,0 +1,12 @@ +{% extends 'layout/base.html.twig' %} + +{% block body %} +
+ {% include 'components/layout/_title_3.html.twig' with {name: 'product.list_name'|trans} %} + {% include 'components/product/_search.html.twig' with {form: search_form} %} + {% include 'components/product/_section.html.twig'with {objects_pagination, services_pagination} %} + {% include 'components/product/_tab_content.html.twig' with {objects_pagination, services_pagination} %} +
+{% endblock %} + +{% block link %}{% endblock %} diff --git a/templates/pages/product/new_object.html.twig b/templates/pages/product/new_object.html.twig new file mode 100644 index 0000000..b93b6bd --- /dev/null +++ b/templates/pages/product/new_object.html.twig @@ -0,0 +1,37 @@ +{% extends 'layout/base.html.twig' %} + +{% form_theme form 'bootstrap_5_layout.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% block body %} +
+ {% include 'components/layout/_title_3.html.twig' with { + name: (i18n_prefix ~ '.title')|trans + } %} + {% if info is defined and not null %} + {% include 'components/layout/_text.html.twig' with { + text: info + } %} + {% endif %} + + {% include 'components/layout/_title_5.html.twig' with { + title: 'new_product.available'|trans, + centerRow: true, + colClass: 'col-md-8 col-lg-6 col-xl-5' + } %} + +
+
+ {{ form_start(form) }} + {% include 'components/product/_object_fields.html.twig' %} + {% include 'components/product/_address_form.html.twig' %} + {% include 'components/product/service_request/_submit.html.twig' with { + submit: form.submit + } %} + {{ form_end(form) }} +
+
+
+{% endblock %} +{% block link %}{% endblock %} diff --git a/templates/pages/product/new_service.html.twig b/templates/pages/product/new_service.html.twig new file mode 100644 index 0000000..58f5f23 --- /dev/null +++ b/templates/pages/product/new_service.html.twig @@ -0,0 +1,42 @@ +{% extends 'layout/base.html.twig' %} + +{% form_theme form 'bootstrap_5_layout.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% block body %} +
+ {% include 'components/layout/_title_3.html.twig' with { + name: (i18n_prefix ~ '.title')|trans + } %} + {% if info is defined and not null %} + {% include 'components/layout/_text.html.twig' with { + text: info + } %} + {% endif %} + + {% include 'components/layout/_title_5.html.twig' with { + title: 'new_product.available'|trans, + centerRow: true, + colClass: 'col-md-8 col-lg-6 col-xl-5' + } %} + +
+
+ {{ form_start(form, { + attr: { + novalidate: true, + } + }) }} + {% include 'components/product/_service_fields.html.twig' %} + {% include 'components/product/_address_form.html.twig' %} + {% include 'components/product/service_request/_submit.html.twig' with { + submit: form.submit + } %} + {{ form_end(form) }} +
+
+
+{% endblock %} + +{% block link %}{% endblock %} diff --git a/templates/pages/product/product_availability.html.twig b/templates/pages/product/product_availability.html.twig new file mode 100644 index 0000000..53c64d2 --- /dev/null +++ b/templates/pages/product/product_availability.html.twig @@ -0,0 +1,47 @@ +{% extends 'layout/base.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% block body %} +
+ {% include 'components/layout/_back_to_link.html.twig' with { + name: 'templates.pages.product.product_availability.back_to_list'|trans, + link: { + name: 'app_user_' ~ product.type.value ~ 's' + } + } %} +
+ {% include 'components/layout/_title_3.html.twig' with { + name: 'templates.pages.product.product_availability.title'|trans + } %} + {{ form_start(form) }} + {% include 'components/product/_calendar.html.twig' with { + title: (i18n_prefix ~ '.subtitle')|trans, + form: form, + actionNeeded: true + } %} + {{ form_end(form) }} +
+ {% if unavailabilities is not empty %} +
+ {% include 'components/layout/_title_3.html.twig' with { + name: (i18n_prefix ~ '.delete_title')|trans + } %} +
+ {% for unavailability in unavailabilities %} +
+ + {{ (i18n_prefix ~ '.date')|trans({ + '%startAt%': unavailability.startAt|date('format.date'|trans({}, 'date')), + '%endAt%': unavailability.endAt|date('format.date'|trans({}, 'date')) + }) }} + + supprimer +
+ {% endfor %} +
+
+ {% endif %} +
+{% endblock %} +{% block link %}{% endblock %} diff --git a/templates/pages/product/show.html.twig b/templates/pages/product/show.html.twig new file mode 100644 index 0000000..9b691a8 --- /dev/null +++ b/templates/pages/product/show.html.twig @@ -0,0 +1,28 @@ +{% extends 'layout/base.html.twig' %} + +{% block body %} + {% include 'components/layout/_back_to_link.html.twig' with { + name: 'templates.pages.product.show.back_to_search'|trans, + link: { + name: 'app_product_list' + } + } %} +
+ {% include 'components/product/_info.html.twig' with {product: product} %} + {% include 'components/product/_lender.html.twig' with { + avatar: product.owner.avatar, + name: product.owner.displayName, + isPlace: product.owner.isPlace, + address: product.owner.address, + schedule: product.owner.schedule + } %} +
+ {% include 'components/product/_calendar.html.twig' with { + title: 'product.show.calendar_title'|trans, + actionNeeded: true + } %} +
+
+{% endblock %} + +{% block link %}{% endblock %} diff --git a/templates/pages/register/step1.html.twig b/templates/pages/register/step1.html.twig new file mode 100644 index 0000000..2bd48d7 --- /dev/null +++ b/templates/pages/register/step1.html.twig @@ -0,0 +1,50 @@ +{% extends 'layout/connection.html.twig' %} + +{% form_theme form 'bootstrap_5_layout.html.twig' %} + +{% trans_default_domain 'security' %} + +{% block error %} + {{ form_errors(form) }} +{% endblock %} + +{% block icon %} + {{ parent() }} +{% endblock %} + +{% block page_title %} + {% with {name: 'account_create_action.title'|trans} %} + {{ parent() }} + {% endwith %} +{% endblock %} + +{% block text %} + {% with {info: 'account_create_action.info'|trans} %} + {{ parent() }} + {% endwith %} +{% endblock %} + +{% block form_element %} + {# + {% if is_granted('IS_AUTHENTICATED_REMEMBERED') %} + {{ 'account_create_action.already-logged'|trans }} + {% else %} + #} + {{ form_start(form, { + attr: { + novalidate: true + } + }) }} + {{ form_widget(form.email) }} + {{ form_errors(form.email) }} +
+ {{ form_widget(form.submit) }} +
+ {{ form_end(form) }} + + {# + {% endif %} + #} +{% endblock %} + +{% block link %}{% endblock %} diff --git a/templates/pages/register/step2.html.twig b/templates/pages/register/step2.html.twig new file mode 100644 index 0000000..3f0e1db --- /dev/null +++ b/templates/pages/register/step2.html.twig @@ -0,0 +1,70 @@ +{% extends 'layout/connection.html.twig' %} + +{% form_theme form 'bootstrap_5_layout.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% trans_default_domain 'security' %} + +{% block error %}{% endblock %} + +{% block page_title %} + {% with {name: 'Créer un compte'} %} + {{ parent() }} + {% endwith %} +{% endblock %} + +{% block text %} + {% set groups_invited = user.getMyGroupsAsInvited %} + {% set info = '' %} + {% if not groups_invited.isEmpty %} + {% set info = (i18n_prefix ~ '.group_invitations')|trans({ + '%groups%': groups_invited|map(p => p.name)|join(', ') + }) ~ '
' %} + {% endif %} + {% set info = info ~ (i18n_prefix ~ '.info')|trans %} + {{ parent() }} +{% endblock %} + +{% block form_element %} + {{ form_errors(form) }} + + {{ form_start(form, {attr: {novalidate: true}}) }} + + {{ form_row(form.type) }} +
+ {{ form_label(form.firstname) }} + {{ form_widget(form.firstname) }} + {{ form_errors(form.firstname) }} + {{ form_label(form.lastname) }} + {{ form_widget(form.lastname) }} + {{ form_errors(form.lastname) }} +
+
+ {{ form_label(form.name) }} + {{ form_widget(form.name) }} + {{ form_errors(form.name) }} +
+ + {% include 'components/form/_password_visibility.html.twig' with { + form: form.plainPassword.first + } %} + + {% include 'components/form/_password_visibility.html.twig' with { + form: form.plainPassword.second + } %} + +
+
+ {{ form_widget(form.gdpr) }} + {{ form_errors(form.gdpr) }} +
+
+ +
+ {{ form_widget(form.submit) }} +
+ {{ form_end(form) }} +{% endblock %} + +{% block link %}{% endblock %} diff --git a/templates/pages/user/account/change_login.html.twig b/templates/pages/user/account/change_login.html.twig new file mode 100644 index 0000000..f1f0aaa --- /dev/null +++ b/templates/pages/user/account/change_login.html.twig @@ -0,0 +1,29 @@ +{% extends 'layout/base.html.twig' %} + +{% form_theme form 'bootstrap_5_layout.html.twig' %} + +{% block body %} +
+ {% include 'components/layout/_title_3.html.twig' with { + name: 'change_login.action.title'|trans + } %} + +
+
+ {{ form_start(form, { + attr: { + novalidate: true, + } + }) }} + {{ form_row(form.email.first) }} + {{ form_row(form.email.second) }} +
+ {{ form_widget(form.submit) }} +
+ {{ form_end(form) }} +
+
+
+{% endblock %} + +{% block link %}{% endblock %} diff --git a/templates/pages/user/account/change_password.html.twig b/templates/pages/user/account/change_password.html.twig new file mode 100644 index 0000000..1d4f016 --- /dev/null +++ b/templates/pages/user/account/change_password.html.twig @@ -0,0 +1,37 @@ +{% extends 'layout/base.html.twig' %} + +{% form_theme form 'bootstrap_5_layout.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + + +{% block body %} +
+ {% include 'components/layout/_title_3.html.twig' with { + name: (i18n_prefix ~ '.title')|trans + } %} + +
+
+ {{ form_start(form, { + attr: { + novalidate: true, + } + }) }} + {{ form_row(form.oldPassword) }} + {% include 'components/form/_password_visibility.html.twig' with { + form: form.plainPassword.first + } %} + {% include 'components/form/_password_visibility.html.twig' with { + form: form.plainPassword.second + } %} +
+ {{ form_widget(form.submit) }} +
+ {{ form_end(form) }} +
+
+
+{% endblock %} + +{% block link %}{% endblock %} diff --git a/templates/pages/user/account/edit_profile.html.twig b/templates/pages/user/account/edit_profile.html.twig new file mode 100644 index 0000000..2ada201 --- /dev/null +++ b/templates/pages/user/account/edit_profile.html.twig @@ -0,0 +1,70 @@ +{% extends 'layout/base.html.twig' %} + +{% form_theme form 'bootstrap_5_layout.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% block error %}{% endblock %} + +{% block body %} +
+ {% include 'components/layout/_title_3.html.twig' with { + name: (i18n_prefix ~ '.title')|trans + } %} +
+
+ {{ form_start(form, { + attr: { + novalidate: true, + } + }) }} + {% if form.firstname is defined %} + {{ form_row(form.firstname) }} + {% endif %} + + {% if form.lastname is defined %} + {{ form_row(form.lastname) }} + {% endif %} + + {# place field #} + {% if form.name is defined %} + {{ form_row(form.name) }} + {% endif %} + + {{ form_row(form.avatar) }} + {% if app.user.avatar is not null %} + {% include 'components/form/_photo_preview.html.twig' with { + image: app.user.avatar, + entity: app.user, + isUser: true, + } %} + {% endif %} + + {# place field #} + {% if form.schedule is defined %} + {{ form_row(form.schedule) }} + {% endif %} + + {% if form.category is defined %} + {{ form_row(form.category) }} + {% endif %} + + {% if form.description is defined %} + {{ form_row(form.description) }} + {% endif %} + + {{ form_row(form.phone) }} + +
+ {{ form_widget(form.smsNotifications) }} +
+ +
+ {{ form_widget(form.submit) }} +
+ {{ form_end(form) }} +
+
+
+{% endblock %} +{% block link %}{% endblock %} diff --git a/templates/pages/user/account/profile.html.twig b/templates/pages/user/account/profile.html.twig new file mode 100644 index 0000000..b814169 --- /dev/null +++ b/templates/pages/user/account/profile.html.twig @@ -0,0 +1,64 @@ +{% extends 'layout/base.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% block body %} +
+
+ {% if user.avatar is null %} + {{ user.displayName }} + {% else %} +
+
+ {% endif %} +
+ + {% include 'components/layout/_title_3.html.twig' with { + name: user.getDisplayName, + rowClass: 'mt-3' + } %} + + {% if user.category is not null %} + {% if user.category.parent is not null %} + {% set category = user.category.parent ~ ' / ' ~ user.category %} + {% else %} + {% set category = user.category %} + {% endif %} + {% include 'components/layout/_text.html.twig' with { + text: category, + textAlign: 'text-center' + } %} + {% endif %} + {% if user.address is not null %} +
+ + {{ user.address.subAndlocality }} + +
+ {% endif %} + {% if user.description is not null %} + {% include 'components/layout/_text.html.twig' with { + margin: 'mt-3', + text: user.description, + } %} + {% endif %} + + {% if objects_pagination is empty and services_pagination is empty %} +
+ {{ (i18n_prefix ~ '.no_result')|trans }} +
+ {% else %} + {% include 'components/product/_section.html.twig' %} + {% include 'components/product/_tab_content.html.twig' with {objects_pagination, services_pagination} %} + {% endif %} +
+{% endblock %} +{% block link %}{% endblock %} diff --git a/templates/pages/user/group/list.html.twig b/templates/pages/user/group/list.html.twig new file mode 100644 index 0000000..9cb646f --- /dev/null +++ b/templates/pages/user/group/list.html.twig @@ -0,0 +1,79 @@ +{% extends 'layout/base.html.twig' %} + +{% set i18n_prefix = _self|i18n_prefix %} + +{% block body %} +
+ {% include 'components/layout/_title_3.html.twig' with { + name: (i18n_prefix ~ '.title')|trans + } %} + + {# include 'components/layout/_searchbar.html.twig' with { + col: 'col col-lg-5', + } #} + +
+ {% if app.user.userGroups is not empty %} + {% if not app.user.getMyUserGroupsAsInvited.empty %} +
+ {# user invitations to groups #} + {% include 'components/layout/_title_5.html.twig' with { + title: (i18n_prefix ~ '.user_invitations')|trans, + rowClass: 'justify-content-center mt-4' + } %} +
+
    + {% for item in app.user.getMyUserGroupsAsInvited %} +
  • + {% include 'components/group/_list-content.html.twig' with { + item, + needAction: true, + isAdmin: item.membership.isAdmin() + } %} +
  • + {% endfor %} +
+
+
+ {% endif %} + + {% if not app.user.getMyUserGroupsAsConfirmed.empty %} +
+ {# user groups #} + {% include 'components/layout/_title_5.html.twig' with { + title: (i18n_prefix ~ '.user_groups')|trans, + rowClass: 'justify-content-center' + } %} +
+
    + {% for item in app.user.getMyUserGroupsAsConfirmed %} +
  • + {% include 'components/group/_list-content.html.twig' with { + item, + needAction: item.membership.isAdmin() ? true : false, + isAdmin: item.membership.isAdmin() + } %} +
  • + {% endfor %} +
+
+
+ {% endif %} + + {% else %} +
+ + {{ (i18n_prefix ~ '.no_group')|trans }} + +
+ {% endif %} +
+ + +
+{% endblock %} +{% block link %}{% endblock %} diff --git a/tests/Api/ApiResource/GroupResourceTest.php b/tests/Api/ApiResource/GroupResourceTest.php new file mode 100644 index 0000000..21df549 --- /dev/null +++ b/tests/Api/ApiResource/GroupResourceTest.php @@ -0,0 +1,37 @@ +request('GET', '/api/groups/stats'); + self::assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); + self::assertJsonEquals([ + '@context' => '/api/contexts/Group', + '@id' => '/api/groups/stats', + '@type' => 'Group', + 'count' => self::COUNT, + ]); + $responseArray = $response->toArray(); + self::assertEqualsCanonicalizing(['@context', '@id', '@type', 'count'], array_keys($responseArray)); + } +} diff --git a/tests/Api/State/Processor/ProductSwitchProcessorTest.php b/tests/Api/State/Processor/ProductSwitchProcessorTest.php new file mode 100644 index 0000000..eae8d3a --- /dev/null +++ b/tests/Api/State/Processor/ProductSwitchProcessorTest.php @@ -0,0 +1,89 @@ +request('PATCH', self::API_URL); + self::assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED); + self::assertJsonContains([ + '@context' => '/api/contexts/Error', + '@type' => 'hydra:Error', + 'hydra:title' => 'An error occurred', + 'hydra:description' => 'Full authentication is required to access this resource.', + ]); + } + + /** + * Incorrect uuid. + */ + public function testProductSwitchProcessorNotFoundFailure(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + $client->request('PATCH', '/api/product/'.TestReference::UUID_404.'/switchStatus'); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + /** + * Object of someone else. + */ + public function testProductSwitchProcessorForbiddenFailure(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + $client->request('PATCH', self::API_URL); + self::assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); + self::assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN); + } + + /** + * Nominal case. + */ + public function testProductSwitchProcessorSuccess(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + + // check initial state + $product = $this->getProductRepository()->get(TestReference::OBJECT_LOIC_1); + self::assertSame($product->getStatus()->value, 'active'); + + // switch + $response = $client->request('PATCH', self::API_URL); + self::assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); + self::assertResponseStatusCodeSame(Response::HTTP_OK); + $responseArray = $response->toArray(); + self::assertSame($responseArray['status'], 'paused'); + + // another switch + $response = $client->request('PATCH', self::API_URL); + self::assertResponseStatusCodeSame(Response::HTTP_OK); + $responseArray = $response->toArray(); + self::assertSame($responseArray['status'], 'active'); + } +} diff --git a/tests/E2E/Controller/AppControllerTest.php b/tests/E2E/Controller/AppControllerTest.php new file mode 100644 index 0000000..ff7844d --- /dev/null +++ b/tests/E2E/Controller/AppControllerTest.php @@ -0,0 +1,23 @@ +request('GET', '/fr/accueil'); + // $client->takeScreenshot('var/screen.jpg'); // the screenshot is stored at the root of the project + self::assertSelectorTextContains('body', 'APES Hauts-de-France'); + } +} diff --git a/tests/Fixtures/images/apes.png b/tests/Fixtures/images/apes.png new file mode 100644 index 0000000..87e1fe2 Binary files /dev/null and b/tests/Fixtures/images/apes.png differ diff --git a/tests/Functional/ChoiceFormFieldTrait.php b/tests/Functional/ChoiceFormFieldTrait.php new file mode 100644 index 0000000..5585ce7 --- /dev/null +++ b/tests/Functional/ChoiceFormFieldTrait.php @@ -0,0 +1,27 @@ +tick(); + } else { + $field->untick(); + } + + return $this; + } +} diff --git a/tests/Functional/Controller/Admin/AdministratorCrudControllerTest.php b/tests/Functional/Controller/Admin/AdministratorCrudControllerTest.php new file mode 100644 index 0000000..8aeeb5d --- /dev/null +++ b/tests/Functional/Controller/Admin/AdministratorCrudControllerTest.php @@ -0,0 +1,139 @@ +loginAsUser16($client); + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', AdministratorCrudController::class)); + self::assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN); + } + + /** + * @see AdministratorCrudController + */ + public function testController(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + + // list + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', AdministratorCrudController::class)); + self::assertResponseIsSuccessful(); + + // edit + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', AdministratorCrudController::class, TestReference::ADMIN_CAMILLE)); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', AdministratorCrudController::class, TestReference::ADMIN_CAMILLE)); + self::assertResponseIsSuccessful(); + + // new + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL, 'new', AdministratorCrudController::class)); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(TestReference::ACTION_SAVE_AND_RETURN)->form(); + $client->submit($form, [ + $form->getName().'[email]' => 'new_admin@example.com', + $form->getName().'[firstname]' => 'Foo', + $form->getName().'[lastname]' => 'Bar', + $form->getName().'[plainPassword][first]' => TestReference::PASSWORD, + $form->getName().'[plainPassword][second]' => TestReference::PASSWORD, + $form->getName().'[enabled]' => true, + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } + + /** + * Test that the default validation is applied for the email field. + */ + public function testNewValidation(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL, 'new', AdministratorCrudController::class)); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(TestReference::ACTION_SAVE_AND_RETURN)->form(); + $client->submit($form, [ + $form->getName().'[email]' => 'a@b', + $form->getName().'[firstname]' => 'Foo', + $form->getName().'[lastname]' => 'Bar', + $form->getName().'[plainPassword][first]' => 'a', + $form->getName().'[plainPassword][second]' => 'a', + $form->getName().'[enabled]' => true, + ]); + self::assertResponseIsSuccessful(); // no 422 with EA + self::assertSelectorTextContains('div.field-email', 'This value is not a valid email address'); + self::assertSelectorTextContains('div', 'This value is too short'); // EA bug: the div class is not correct + } + + /** + * @see AbstractUserCrudController::connectAs() + */ + public function testConnectAs(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'connectAs', AdministratorCrudController::class, TestReference::ADMIN_CAMILLE)); + self::assertResponseRedirects(); + $client->followRedirects(); + self::assertResponseRedirects(); + } + + /** + * @see AbstractUserCrudController::export() + */ + public function testExport(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', AdministratorCrudController::class)); + self::assertResponseIsSuccessful(); + + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'export', AdministratorCrudController::class)); + self::assertResponseIsSuccessful(); + } + + public function testDeleteSuccess(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'delete', AdministratorCrudController::class, TestReference::PLACE_7)); + self::assertResponseRedirects(); + $client->followRedirects(); + self::assertResponseRedirects(); + } + + /** + * Can't delete the main admin account. + * + * @see AdministratorCrudController::delete() + */ + public function testDeleteAccessDeniedException(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'delete', AdministratorCrudController::class, TestReference::ADMIN_CAMILLE)); + self::assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN); + } +} diff --git a/tests/Functional/Controller/Admin/CategoryObjectCrudControllerTest.php b/tests/Functional/Controller/Admin/CategoryObjectCrudControllerTest.php new file mode 100644 index 0000000..b1e98b6 --- /dev/null +++ b/tests/Functional/Controller/Admin/CategoryObjectCrudControllerTest.php @@ -0,0 +1,82 @@ +loginAsAdmin($client); + + // list + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', CategoryObjectCrudController::class)); + self::assertResponseIsSuccessful(); + + // edit + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', CategoryObjectCrudController::class, TestReference::CATEGORY_OBJECT_1)); + self::assertResponseIsSuccessful(); + + // upload + $form = $crawler->selectButton(self::SAVE_AND_CONTINUE_BUTTON_NAME)->form(); + $image = realpath(__DIR__.'/../../../Fixtures/images/apes.png'); + /** @var FileFormField $fileFormField */ + $fileFormField = $form[$form->getName().'[image][file]']; + $fileFormField->upload((string) $image); + + $client->submit($form); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + + // delete file + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', CategoryObjectCrudController::class, TestReference::CATEGORY_OBJECT_1)); + self::assertResponseIsSuccessful(); + $form = $crawler->selectButton(self::SAVE_AND_CONTINUE_BUTTON_NAME)->form(); + /** @var ChoiceFormField $choiceFormField */ + $choiceFormField = $form[$form->getName().'[image][delete]']; + $choiceFormField->tick(); + $client->submit($form); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', CategoryObjectCrudController::class, TestReference::CATEGORY_OBJECT_1)); + self::assertResponseIsSuccessful(); + + // new + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'new', CategoryObjectCrudController::class)); + self::assertResponseIsSuccessful(); + + // move up + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'moveUp', CategoryObjectCrudController::class, TestReference::CATEGORY_OBJECT_1)); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + + // move down + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'moveDown', CategoryObjectCrudController::class, TestReference::CATEGORY_OBJECT_1)); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/CategoryServiceCrudControllerTest.php b/tests/Functional/Controller/Admin/CategoryServiceCrudControllerTest.php new file mode 100644 index 0000000..bfde891 --- /dev/null +++ b/tests/Functional/Controller/Admin/CategoryServiceCrudControllerTest.php @@ -0,0 +1,49 @@ +loginAsAdmin($client); + + // list + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', CategoryServiceCrudController::class)); + self::assertResponseIsSuccessful(); + + // edit + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', CategoryServiceCrudController::class, TestReference::CATEGORY_SERVICE_1)); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', CategoryServiceCrudController::class, TestReference::CATEGORY_SERVICE_1)); + self::assertResponseIsSuccessful(); + + // new + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL, 'new', CategoryServiceCrudController::class)); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(TestReference::ACTION_SAVE_AND_RETURN)->form(); + $client->submit($form, [ + $form->getName().'[name]' => 'category service foo', + $form->getName().'[parent]' => TestReference::CATEGORY_SERVICE_1, + $form->getName().'[enabled]' => 1, + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/DashboardControllerAsGroupAdminTest.php b/tests/Functional/Controller/Admin/DashboardControllerAsGroupAdminTest.php new file mode 100644 index 0000000..4cd026c --- /dev/null +++ b/tests/Functional/Controller/Admin/DashboardControllerAsGroupAdminTest.php @@ -0,0 +1,29 @@ +loginAsUser16($client); + $client->request('GET', '/admin'); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/DashboardControllerTest.php b/tests/Functional/Controller/Admin/DashboardControllerTest.php new file mode 100644 index 0000000..992b2f7 --- /dev/null +++ b/tests/Functional/Controller/Admin/DashboardControllerTest.php @@ -0,0 +1,24 @@ +loginAsAdmin($client); + $client->request('GET', '/admin'); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/Dev/DevToolsControllerTest.php b/tests/Functional/Controller/Admin/Dev/DevToolsControllerTest.php new file mode 100644 index 0000000..4b3aa48 --- /dev/null +++ b/tests/Functional/Controller/Admin/Dev/DevToolsControllerTest.php @@ -0,0 +1,25 @@ +loginAsAdmin($client); + $client->request('GET', sprintf(TestReference::ADMIN_URL_CUSTOM_CONTROLLER, 'admin_dev_tools')); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/FooterCrudControllerTest.php b/tests/Functional/Controller/Admin/FooterCrudControllerTest.php new file mode 100644 index 0000000..fae05e2 --- /dev/null +++ b/tests/Functional/Controller/Admin/FooterCrudControllerTest.php @@ -0,0 +1,39 @@ +loginAsAdmin($client); + + $menuRepo = $client->getContainer()->get(MenuRepository::class); + /** @var Menu $menu */ + $menu = $menuRepo->getByCode('footer'); + + // list + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', FooterCrudController::class)); + self::assertResponseIsSuccessful(); + + // edit + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', FooterCrudController::class, $menu->getId())); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', FooterCrudController::class, $menu->getId())); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/FooterItemCrudControllerTest.php b/tests/Functional/Controller/Admin/FooterItemCrudControllerTest.php new file mode 100644 index 0000000..8d88d47 --- /dev/null +++ b/tests/Functional/Controller/Admin/FooterItemCrudControllerTest.php @@ -0,0 +1,107 @@ +followRedirects(); + + $this->loginAsAdmin($client); + + $dataId = sprintf("[data-id='%s']", TestReference::MENU_FOOTER_ITEM_FIRST); + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', MenuItemFooterCrudController::class)); + + self::assertSelectorTextNotContains($dataId, 'menu.action.up_item'); + self::assertSelectorTextContains($dataId, 'menu.action.down_item'); + + $downLink = $client->getCrawler()->filter($dataId.' .action-down')->link(); + $client->click($downLink); + + self::assertSelectorTextContains($dataId, 'menu.action.up_item'); + self::assertSelectorTextContains($dataId, 'menu.action.down_item'); + } + + /** + * @see AbstractMenuItemCrudController::moveUpPosition() + */ + public function testMoveUpPosition(): void + { + $client = self::createClient(); + $client->followRedirects(); + $this->loginAsAdmin($client); + + $dataId = sprintf("[data-id='%s']", TestReference::MENU_FOOTER_ITEM_LAST); + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', MenuItemFooterCrudController::class)); + + self::assertSelectorTextContains($dataId, 'menu.action.up_item'); + $upLink = $client->getCrawler()->filter($dataId.' .action-up')->link(); + $client->click($upLink); + self::assertSelectorTextContains($dataId, 'menu.action.up_item'); + self::assertSelectorTextContains($dataId, 'menu.action.down_item'); + } + + public function testController(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + + // list + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', MenuItemFooterCrudController::class)); + self::assertResponseIsSuccessful(); + + // edit + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', MenuItemFooterCrudController::class, TestReference::MENU_FOOTER_ITEM_FIRST)); + self::assertResponseIsSuccessful(); + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', MenuItemFooterCrudController::class, TestReference::MENU_FOOTER_ITEM_LAST)); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', MenuItemFooterCrudController::class, TestReference::MENU_FOOTER_ITEM_FIRST)); + self::assertResponseIsSuccessful(); + + // new icon link + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL, 'new', MenuItemMenuSocialNetwordFooterCrudController::class)); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(TestReference::ACTION_SAVE_AND_RETURN)->form(); + $client->submit($form, [ + $form->getName().'[mediaType]' => 'facebook', + $form->getName().'[link]' => '/bar', + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + + // new text link + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL, 'new', NewMenuFooterLinkController::class)); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(TestReference::ACTION_SAVE_AND_RETURN)->form(); + $client->submit($form, [ + $form->getName().'[name]' => 'foo', + $form->getName().'[link]' => '/bar', + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/GroupCrudControllerAsGroupAdminTest.php b/tests/Functional/Controller/Admin/GroupCrudControllerAsGroupAdminTest.php new file mode 100644 index 0000000..dd80e90 --- /dev/null +++ b/tests/Functional/Controller/Admin/GroupCrudControllerAsGroupAdminTest.php @@ -0,0 +1,33 @@ +loginAsUser16($client); + + // list + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', GroupCrudController::class)); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/GroupCrudControllerTest.php b/tests/Functional/Controller/Admin/GroupCrudControllerTest.php new file mode 100644 index 0000000..cc5f8e9 --- /dev/null +++ b/tests/Functional/Controller/Admin/GroupCrudControllerTest.php @@ -0,0 +1,121 @@ +loginAsAdmin($client); + + // list + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', GroupCrudController::class)); + self::assertResponseIsSuccessful(); + + // list + filter + $filters = 'filters[type]=private&filter[membership]=free'; + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', GroupCrudController::class.'&'.$filters)); + self::assertResponseIsSuccessful(); + + // edit + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', GroupCrudController::class, TestReference::GROUP_1)); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', GroupCrudController::class, TestReference::GROUP_1)); + self::assertResponseIsSuccessful(); + + // new + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL, 'new', GroupCrudController::class)); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(TestReference::ACTION_SAVE_AND_RETURN)->form(); + $client->submit($form, [ + $form->getName().'[name]' => 'Groupe public', + $form->getName().'[type]' => 'public', + $form->getName().'[description]' => 'very nice group', + $form->getName().'[url]' => 'https://www.example.com', + $form->getName().'[membership]' => 'free', + $form->getName().'[invitationByAdmin]' => true, + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } + + /** + * @return iterable + */ + public function provideTestInviateActionSuccess(): iterable + { + yield [TestReference::GROUP_1, 'userinvited@example.com', false]; // Invite a user that isn't already in the database. + yield [TestReference::GROUP_1, TestReference::USER_17_EMAIL, true]; // Invite a user that is already in the database + } + + /** + * Invite a user that isn't already in the database. + * + * @dataProvider provideTestInviateActionSuccess + * + * @see GroupCrudController::invite() + */ + public function testInviteActionSuccess(string $groupId, string $email, bool $hasNotification): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + + // custom invite + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'invite', GroupCrudController::class, $groupId)); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton('group_invitation_form_submit')->form(); + $client->submit($form, [ + $form->getName().'[email]' => $email, + ]); + self::assertEmailCount(1); + // only for existing users + if ($hasNotification) { + self::assertNotificationCount(1); + } + + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', 'app.controller.admin.group_crud_controller.invite.flash.success'); + } + + public function testOffersListButtonOnIndex(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', GroupCrudController::class)); + $dataId = sprintf("[data-id='%s']", TestReference::GROUP_1); + $offersListlink = $client->getCrawler()->filter($dataId.' .action-offersList')->link(); + $client->click($offersListlink); + self::assertResponseRedirects(); + $client->followRedirect(); + } + + public function testExport(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'export', GroupCrudController::class)); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/GroupOfferCrudControllerAsGroupAdminTest.php b/tests/Functional/Controller/Admin/GroupOfferCrudControllerAsGroupAdminTest.php new file mode 100644 index 0000000..39157b2 --- /dev/null +++ b/tests/Functional/Controller/Admin/GroupOfferCrudControllerAsGroupAdminTest.php @@ -0,0 +1,36 @@ +loginAsUser16($client); + + // list+custom filters + $filters = '&filters[group]='.TestReference::GROUP_1; + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', GroupOfferCrudController::class.'&'.$filters)); + self::assertResponseIsSuccessful(); + + // new (groups are restricted) + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'new', GroupOfferCrudController::class)); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/GroupOfferCrudControllerTest.php b/tests/Functional/Controller/Admin/GroupOfferCrudControllerTest.php new file mode 100644 index 0000000..4c48dc1 --- /dev/null +++ b/tests/Functional/Controller/Admin/GroupOfferCrudControllerTest.php @@ -0,0 +1,56 @@ +loginAsAdmin($client); + + // list+custom filters + $filters = '&filters[group]='.TestReference::GROUP_1; + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', GroupOfferCrudController::class.'&'.$filters)); + self::assertResponseIsSuccessful(); + + // edit + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', GroupOfferCrudController::class, TestReference::GROUP_OFFER_GROUP_1_1)); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', GroupOfferCrudController::class, TestReference::GROUP_OFFER_GROUP_1_1)); + self::assertResponseIsSuccessful(); + + // new + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL, 'new', GroupOfferCrudController::class)); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(TestReference::ACTION_SAVE_AND_RETURN)->form(); + $client->submit($form, [ + $form->getName().'[group]' => TestReference::GROUP_1, + $form->getName().'[name]' => 'New special offer', + $form->getName().'[type]' => 'monthly', + $form->getName().'[price]' => 66, + $form->getName().'[currency]' => 'EUR', + $form->getName().'[active]' => false, + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/MenuCrudControllerTest.php b/tests/Functional/Controller/Admin/MenuCrudControllerTest.php new file mode 100644 index 0000000..6b7818d --- /dev/null +++ b/tests/Functional/Controller/Admin/MenuCrudControllerTest.php @@ -0,0 +1,39 @@ +loginAsAdmin($client); + + $menuRepo = $client->getContainer()->get(MenuRepository::class); + /** @var Menu $menu */ + $menu = $menuRepo->getByCode('menu'); + + // list + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', MenuCrudController::class)); + self::assertResponseIsSuccessful(); + + // edit + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', MenuCrudController::class, $menu->getId())); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', MenuCrudController::class, $menu->getId())); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/MenuItemCrudControllerTest.php b/tests/Functional/Controller/Admin/MenuItemCrudControllerTest.php new file mode 100644 index 0000000..defb0da --- /dev/null +++ b/tests/Functional/Controller/Admin/MenuItemCrudControllerTest.php @@ -0,0 +1,108 @@ +followRedirects(); + + $this->loginAsAdmin($client); + + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', MenuItemCrudController::class)); + + $dataId = sprintf("[data-id='%s']", TestReference::MENU_HEADER_ITEM_FIRST); + self::assertSelectorTextNotContains($dataId, 'menu.action.up_item'); + self::assertSelectorTextContains($dataId, 'menu.action.down_item'); + + $client->clickLink('menu.action.down_item'); + + self::assertSelectorTextContains($dataId, 'menu.action.up_item'); + self::assertSelectorTextContains($dataId, 'menu.action.down_item'); + } + + /** + * @see AbstractMenuItemCrudController::moveUpPosition() + */ + public function testMoveUpPosition(): void + { + $client = self::createClient(); + $client->followRedirects(); + + $this->loginAsAdmin($client); + + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', MenuItemCrudController::class)); + $dataId = sprintf("[data-id='%s']", TestReference::MENU_HEADER_ITEM_LAST); + + self::assertSelectorTextContains($dataId, 'menu.action.up_item'); + $upLink = $client->getCrawler()->filter($dataId.' .action-up')->link(); + $client->click($upLink); + + self::assertSelectorTextContains($dataId, 'menu.action.up_item'); + self::assertSelectorTextContains($dataId, 'menu.action.down_item'); + } + + public function testController(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + + // list + filter + $filters = 'filters[mediaType]=facebook'; + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&'.$filters, 'index', MenuItemCrudController::class)); + self::assertResponseIsSuccessful(); + + // edit + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', MenuItemCrudController::class, TestReference::MENU_HEADER_ITEM_FIRST)); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', MenuItemCrudController::class, TestReference::MENU_HEADER_ITEM_FIRST)); + self::assertResponseIsSuccessful(); + + // new icon link + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL, 'new', MenuItemSocialNetworkCrudController::class)); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(TestReference::ACTION_SAVE_AND_RETURN)->form(); + $client->submit($form, [ + $form->getName().'[mediaType]' => 'facebook', + $form->getName().'[link]' => '/bar', + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + + // new text link + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL, 'new', MenuItemLinkCrudController::class)); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(TestReference::ACTION_SAVE_AND_RETURN)->form(); + $client->submit($form, [ + $form->getName().'[name]' => 'foo', + $form->getName().'[link]' => '/bar', + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/ObjectCrudControllerTest.php b/tests/Functional/Controller/Admin/ObjectCrudControllerTest.php new file mode 100644 index 0000000..c8bef78 --- /dev/null +++ b/tests/Functional/Controller/Admin/ObjectCrudControllerTest.php @@ -0,0 +1,88 @@ +followRedirects(); + $this->loginAsAdmin($client); + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', ObjectCrudController::class)); + $client->clickLink(self::ACTION_ACTIVATE); + self::assertResponseIsSuccessful(); + $client->clickLink(self::ACTION_ONBREAK); + self::assertResponseIsSuccessful(); + } + + public function testAvailabilityProductButton(): void + { + $client = self::createClient(); + $client->followRedirects(); + $this->loginAsAdmin($client); + + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', ObjectCrudController::class, TestReference::OBJECT_LOIC_1)); + $link = $crawler->selectLink('action.availability')->link(); + self::assertSame('http://localhost/admin?crudAction=linkToProductAvailabilityPage&crudControllerFqcn=App%5CController%5CAdmin%5CObjectCrudController&entityId='.TestReference::OBJECT_LOIC_1.'&referrer=?crudAction%3Ddetail%26crudControllerFqcn%3DApp%255CController%255CAdmin%255CObjectCrudController%26entityId%3D'.TestReference::OBJECT_LOIC_1, $client->click($link)->getUri()); + } + + /** + * @see ObjectCrudController + */ + public function testController(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + + // list+filter + $filters = 'filters[enabled]=1&filters[id]='.TestReference::OBJECT_LOIC_1; + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', ObjectCrudController::class.'&'.$filters)); + self::assertResponseIsSuccessful(); + + // edit + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', ObjectCrudController::class, TestReference::OBJECT_LOIC_1)); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', ObjectCrudController::class, TestReference::OBJECT_LOIC_1)); + self::assertResponseIsSuccessful(); + + // new + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL, 'new', ObjectCrudController::class)); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(TestReference::ACTION_SAVE_AND_RETURN)->form(); + $client->submit($form, [ + $form->getName().'[name]' => 'Object public', + $form->getName().'[status]' => 'paused', + $form->getName().'[visibility]' => 'public', + $form->getName().'[owner]' => TestReference::ADMIN_LOIC, + $form->getName().'[description]' => 'very nice object', + $form->getName().'[age]' => 'some age ago', + $form->getName().'[deposit]' => 300, + $form->getName().'[currency]' => 'EUR', + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/PageCrudControllerTest.php b/tests/Functional/Controller/Admin/PageCrudControllerTest.php new file mode 100644 index 0000000..c80a285 --- /dev/null +++ b/tests/Functional/Controller/Admin/PageCrudControllerTest.php @@ -0,0 +1,44 @@ +loginAsAdmin($client); + + // list + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', PageCrudController::class)); + self::assertResponseIsSuccessful(); + + // edit + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', PageCrudController::class, TestReference::PAGE_1)); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', PageCrudController::class, TestReference::PAGE_1)); + self::assertResponseIsSuccessful(); + + $client->clickLink('page.action.link'); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + + // new + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'new', PageCrudController::class)); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/ParametersControllerTest.php b/tests/Functional/Controller/Admin/ParametersControllerTest.php new file mode 100644 index 0000000..e4f6ba9 --- /dev/null +++ b/tests/Functional/Controller/Admin/ParametersControllerTest.php @@ -0,0 +1,79 @@ +loginAsAdmin($client); + + // form + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL_CUSTOM_CONTROLLER, 'admin_parameters')); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton('parameters_form_submit')->form(); + + self::assertSame(5, $crawler->filter('input:checked')->count()); + + /** @var FormField $notificationsSenderEmailField */ + $notificationsSenderEmailField = $form->get('parameters_form[notificationsSenderEmail]'); + + /** @var FormField $notificationsSenderName */ + $notificationsSenderName = $form->get('parameters_form[notificationsSenderName]'); + + /** @var FormField $groupsCreationMode */ + $groupsCreationMode = $form->get('parameters_form[groupsCreationMode]'); + + /** @var FormField $contactFormEmail */ + $contactFormEmail = $form->get('parameters_form[contactFormEmail]'); + + self::assertSame('info@example.com', $notificationsSenderEmailField->getValue()); + self::assertSame('Contact', $notificationsSenderName->getValue()); + self::assertSame('all', $groupsCreationMode->getValue()); + self::assertSame('info@example.com', $contactFormEmail->getValue()); + + $this->tick($form, $form->getName().'[contactFormEnabled]', false) + ->tick($form, $form->getName().'[groupsEnabled]', false) + ->tick($form, $form->getName().'[groupsPaying]', false) + ->tick($form, $form->getName().'[confidentialityConversationAdminAccess]', false); + + $client->submit($form, [ + $form->getName().'[notificationsSenderEmail]' => 'test@example.com', + $form->getName().'[notificationsSenderName]' => 'Contact test', + $form->getName().'[contactFormEmail]' => 'test@example.com', + $form->getName().'[groupsCreationMode]' => 'only_admin', + ]); + + self::assertResponseRedirects(); + $crawler = $client->followRedirect(); + self::assertResponseIsSuccessful(); + + self::assertSame(1, $crawler->filter('input:checked')->count()); + + $form = $crawler->selectButton('parameters_form_submit')->form(); + /** @var FormField $groupsCreationModeField */ + $groupsCreationModeField = $form->get('parameters_form[groupsCreationMode]'); + self::assertSame('only_admin', $groupsCreationModeField->getValue()); + } +} diff --git a/tests/Functional/Controller/Admin/PaymentCrudControllerTest.php b/tests/Functional/Controller/Admin/PaymentCrudControllerTest.php new file mode 100644 index 0000000..6f7501d --- /dev/null +++ b/tests/Functional/Controller/Admin/PaymentCrudControllerTest.php @@ -0,0 +1,33 @@ +loginAsAdmin($client); + + // list + filters + $filters = '&filters[user]='.TestReference::USER_16; + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', PaymentCrudController::class).'&'.$filters); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', PaymentCrudController::class, TestReference::PAYMENT_USER_16_1)); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/PlaceCrudControllerTest.php b/tests/Functional/Controller/Admin/PlaceCrudControllerTest.php new file mode 100644 index 0000000..9975cb7 --- /dev/null +++ b/tests/Functional/Controller/Admin/PlaceCrudControllerTest.php @@ -0,0 +1,36 @@ +loginAsAdmin($client); + + // list + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', PlaceCrudController::class)); + self::assertResponseIsSuccessful(); + + // edit + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', PlaceCrudController::class, TestReference::PLACE_7)); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', PlaceCrudController::class, TestReference::PLACE_7)); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/ServiceCrudControllerTest.php b/tests/Functional/Controller/Admin/ServiceCrudControllerTest.php new file mode 100644 index 0000000..3df113d --- /dev/null +++ b/tests/Functional/Controller/Admin/ServiceCrudControllerTest.php @@ -0,0 +1,55 @@ +loginAsAdmin($client); + + // list + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', ServiceCrudController::class)); + self::assertResponseIsSuccessful(); + + // edit + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', ServiceCrudController::class, TestReference::SERVICE_LOIC_1)); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', ServiceCrudController::class, TestReference::SERVICE_LOIC_1)); + self::assertResponseIsSuccessful(); + + // new + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL, 'new', ServiceCrudController::class)); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(TestReference::ACTION_SAVE_AND_RETURN)->form(); + $client->submit($form, [ + $form->getName().'[name]' => 'Object public', + $form->getName().'[visibility]' => 'public', + $form->getName().'[status]' => 'active', + $form->getName().'[owner]' => TestReference::ADMIN_LOIC, + $form->getName().'[description]' => 'very nice object', + $form->getName().'[duration]' => '1 hour', + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/ServiceRequestCrudControllerTest.php b/tests/Functional/Controller/Admin/ServiceRequestCrudControllerTest.php new file mode 100644 index 0000000..f93fed2 --- /dev/null +++ b/tests/Functional/Controller/Admin/ServiceRequestCrudControllerTest.php @@ -0,0 +1,58 @@ +loginAsAdmin($client); + + // list + filter + $filters = 'filters[status]=new'; + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', ServiceRequestCrudController::class.'&'.$filters)); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', ServiceRequestCrudController::class, TestReference::SERVICE_REQUEST_1)); + self::assertResponseIsSuccessful(); + + // conversation + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'conversation', ServiceRequestCrudController::class, TestReference::SERVICE_REQUEST_1)); + self::assertResponseIsSuccessful(); + } + + public function testConversationPageWithAccessDenied(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + + $configurationRepo = $this->getConfigurationRepository(); + $config = $configurationRepo->getInstanceConfiguration()?->getConfiguration(); + $config['confidentiality']['confidentialityConversationAdminAccess'] = false; + + $configurationRepo->getInstanceConfiguration()?->setConfiguration($config); + + // conversation page not allowed if confidentiality conversation admin access is set to false + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'conversation', ServiceRequestCrudController::class, TestReference::SERVICE_REQUEST_1)); + self::assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN); + } +} diff --git a/tests/Functional/Controller/Admin/UserCrudControllerTest.php b/tests/Functional/Controller/Admin/UserCrudControllerTest.php new file mode 100644 index 0000000..f8f68e5 --- /dev/null +++ b/tests/Functional/Controller/Admin/UserCrudControllerTest.php @@ -0,0 +1,93 @@ +loginAsAdmin($client); + + // list + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', UserCrudController::class)); + + // list + filters + $filters = '&filters[group]='.TestReference::GROUP_1; + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', UserCrudController::class).'&'.$filters); + self::assertResponseIsSuccessful(); + + // edit + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', UserCrudController::class, TestReference::USER_17)); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', UserCrudController::class, TestReference::USER_16)); + self::assertResponseIsSuccessful(); + } + + /** + * Test that the validation is applied for the phone field. + */ + public function testValidation(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', UserCrudController::class, TestReference::USER_17)); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(TestReference::ACTION_SAVE)->form(); + $client->submit($form, [ + $form->getName().'[phone]' => 'foobar', + ]); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('div', 'app.controller.admin.abstract_user_crud_controller.field.phone.help'); + } + + /** + * Nominal case for the edit action. + */ + public function testEditAction(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', UserCrudController::class, TestReference::USER_17)); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(TestReference::ACTION_SAVE)->form(); + $client->submit($form, [ + $form->getName().'[phone]' => '+33610010203', + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextNotContains('div', 'app.controller.admin.abstract_user_crud_controller.field.phone.help'); + } + + public function testPromoteToAdmin(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $crawler = $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', UserCrudController::class)); + + $dataId = sprintf("[data-id='%s']", TestReference::USER_17); + self::assertSelectorTextContains($dataId, 'action.promoteToAdmin'); + + $link = $crawler->filter('.action-promoteToAdmin')->link(); + $client->click($link); + + self::assertResponseRedirects(); + } +} diff --git a/tests/Functional/Controller/Admin/UserGroupCrudControllerAsGroupAdminTest.php b/tests/Functional/Controller/Admin/UserGroupCrudControllerAsGroupAdminTest.php new file mode 100644 index 0000000..fb210a6 --- /dev/null +++ b/tests/Functional/Controller/Admin/UserGroupCrudControllerAsGroupAdminTest.php @@ -0,0 +1,36 @@ +loginAsUser16($client); + + // list+custom filters + $filters = '&filters[group]='.TestReference::GROUP_1.'&filters[user]='.TestReference::ADMIN_CAMILLE; + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', UserGroupCrudController::class.'&'.$filters)); + self::assertResponseIsSuccessful(); + + // new (groups & users are restricted) + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'new', UserGroupCrudController::class)); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Admin/UserGroupCrudControllerTest.php b/tests/Functional/Controller/Admin/UserGroupCrudControllerTest.php new file mode 100644 index 0000000..cfd08e7 --- /dev/null +++ b/tests/Functional/Controller/Admin/UserGroupCrudControllerTest.php @@ -0,0 +1,65 @@ +loginAsAdmin($client); + + // list+custom filters + $filters = '&filters[group]='.TestReference::GROUP_1; + $client->request('GET', sprintf(TestReference::ADMIN_URL, 'index', UserGroupCrudController::class.'&'.$filters)); + self::assertResponseIsSuccessful(); + + // detail + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'detail', UserGroupCrudController::class, TestReference::USER_GROUP_LOIC_GROUP_7)); + self::assertResponseIsSuccessful(); + } + + /** + * @return iterable + */ + public function provideEditFormSuccess(): iterable + { + yield ['admin', false]; + yield ['admin', true]; + } + + /** + * @dataProvider provideEditFormSuccess + */ + public function testEditFormSuccess(string $role, bool $mainAdminAccount): void + { + $client = self::createClient(); + $this->loginAsSarah($client); + $client->request('GET', sprintf(TestReference::ADMIN_URL.'&entityId=%s', 'edit', UserGroupCrudController::class, TestReference::USER_GROUP_LOIC_GROUP_7)); + + $form = $client->getCrawler()->selectButton('ea[newForm][btn]')->form(); + $client->submit($form, [ + $form->getName().'[membership]' => $role, + $form->getName().'[mainAdminAccount]' => $mainAdminAccount, + ]); + self::assertEmailCount(1); + self::assertNotificationCount(1); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/AppControllerTest.php b/tests/Functional/Controller/AppControllerTest.php new file mode 100644 index 0000000..20aed4b --- /dev/null +++ b/tests/Functional/Controller/AppControllerTest.php @@ -0,0 +1,52 @@ +request('GET', '/'); + self::assertResponseRedirects('/fr'); + } + + /** + * @see AppController::home() + */ + public function testHome(): void + { + $client = self::createClient(); + $client->request('GET', '/fr'); + self::assertResponseIsSuccessful(); + } + + /** + * @see AppController::ping() + */ + public function testPing(): void + { + $client = self::createClient(); + $client->request('GET', '/ping'); + self::assertResponseIsSuccessful(); + } + + /** + * @see ErrorHandler::handleException + */ + public function test404(): void + { + $client = self::createClient(); + $client->request('GET', '/404'); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } +} diff --git a/tests/Functional/Controller/Cms/CmsControllerTest.php b/tests/Functional/Controller/Cms/CmsControllerTest.php new file mode 100644 index 0000000..0146ac1 --- /dev/null +++ b/tests/Functional/Controller/Cms/CmsControllerTest.php @@ -0,0 +1,40 @@ +request('GET', '/fr/qui-sommes-nous'); + self::assertResponseIsSuccessful(); + } + + public function testPageNotFound(): void + { + $client = self::createClient(); + $client->request('GET', '/fr/foobar'); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + /** + * Error case when asking for a not enabled locale. + * + * @see config/packages/framework.yaml + */ + public function testPageLocaleNotEnabled(): void + { + $client = self::createClient(); + $client->request('GET', '/de/qui-sommes-nous'); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } +} diff --git a/tests/Functional/Controller/Group/CreateGroupActionTest.php b/tests/Functional/Controller/Group/CreateGroupActionTest.php new file mode 100644 index 0000000..046eb95 --- /dev/null +++ b/tests/Functional/Controller/Group/CreateGroupActionTest.php @@ -0,0 +1,57 @@ +loginAsUser16($client); + $crawler = $client->request('GET', '/fr/mon-compte/groupes/creer-mon-groupe'); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton('create_group_form_submit')->form(); + $client->submit($form, [ + $form->getName().'[name]' => 'Groupe 1', + $form->getName().'[type]' => 'public', + $form->getName().'[parent]' => TestReference::GROUP_1, + ]); + self::assertResponseRedirects(); + self::assertTrue(u($client->getResponse()->headers->get('Location'))->startsWith('http://localhost/admin')); + + $repo = $this->getGroupRepository(); + self::assertCount(TestReference::GROUP_COUNT + 1, $repo->findAll()); + $repo = $this->getUserGroupRepository(); + self::assertCount(TestReference::USER_GROUP_COUNT + 1, $repo->findAll()); + } + + public function testNewGroupNotAllowed(): void + { + $client = self::createClient(); + $this->loginAsUser16($client); + $config = $this->getConfigurationRepository()->getInstanceConfigurationOrCreate(); + $config->setGroupsCreationModeToAdminOnly(); + + $client->request('GET', '/fr/mon-compte/groupes/creer-mon-groupe'); + self::assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN); + } +} diff --git a/tests/Functional/Controller/Group/GroupControllerTest.php b/tests/Functional/Controller/Group/GroupControllerTest.php new file mode 100644 index 0000000..d89d50b --- /dev/null +++ b/tests/Functional/Controller/Group/GroupControllerTest.php @@ -0,0 +1,101 @@ +request('GET', self::ROUTE_LIST); + self::assertResponseIsSuccessful(); + } + + public function testNotFoundError(): void + { + $client = self::createClient(); + $client->request('GET', self::ROUTE_LIST.'/foobar/'.TestReference::UUID_404); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + public function testShowSuccess(): void + { + $client = self::createClient(); + $client->request('GET', self::ROUTE_LIST.'/group-1/'.TestReference::GROUP_1); + self::assertResponseIsSuccessful(); + } + + public function testShowLoggedSuccess(): void + { + $client = self::createClient(); + $this->loginAsUser11($client); + $client->request('GET', self::ROUTE_LIST.'/group-1/'.TestReference::GROUP_1.'/invitation'); + self::assertResponseIsSuccessful(); + } + + public function testSearchGroup(): void + { + $client = self::createClient(); + $client->request('GET', self::ROUTE_LIST); + self::assertSame(9, $client->getCrawler()->filter('.group-test')->count()); + + $form = $client->getCrawler()->selectButton('group_select_form_submit')->form(); + $client->submit($form, [ + $form->getName().'[q]' => 'Groupe 2', + ]); + + self::assertSame(1, $client->getCrawler()->filter('.group-test')->count()); + } + + public function testMemberList(): void + { + $client = self::createClient(); + $client->request('GET', self::ROUTE_LIST.'/group-1/'.TestReference::GROUP_1.'/membres'); + self::assertResponseIsSuccessful(); + } + + public function testSearchMember(): void + { + $client = self::createClient(); + $crawler = $client->request('GET', self::ROUTE_LIST.'/group-1/'.TestReference::GROUP_1.'/membres'); + self::assertSame(TestReference::GROUP_1_MEMBER_COUNT, $crawler->filter('.group-test')->count()); + + $form = $client->getCrawler()->selectButton('group_select_form_submit')->form(); + $client->submit($form, [ + $form->getName().'[q]' => 'sarah', + ]); + + self::assertSame(1, $client->getCrawler()->filter('.group-test')->count()); + } + + public function testDisplayAdminButton(): void + { + $client = self::createClient(); + + $this->loginAsUser16($client); + $client->request('GET', self::ROUTE_LIST.'/group-1/'.TestReference::GROUP_1.'/membres'); + self::assertSame(1, $client->getCrawler()->filter('.admin-button-test')->count()); + + $this->loginAsSarah($client); + $client->request('GET', self::ROUTE_LIST.'/group-1/'.TestReference::GROUP_1.'/membres'); + self::assertSame(0, $client->getCrawler()->filter('.admin-button-test')->count()); + } +} diff --git a/tests/Functional/Controller/Payment/PrepareActionTest.php b/tests/Functional/Controller/Payment/PrepareActionTest.php new file mode 100644 index 0000000..a71cb74 --- /dev/null +++ b/tests/Functional/Controller/Payment/PrepareActionTest.php @@ -0,0 +1,64 @@ +loginAsAdmin($client); + $client->request('POST', '/en/my-account/payment/'.TestReference::GROUP_OFFER_GROUP_1_1.'/prepare'); + self::assertResponseIsUnprocessable(); + } + + /** + * Group offer ot found. + */ + public function testAcceptInvitationNotFoundException(): void + { + $client = self::createClient(); + $this->loginAsUser11($client); + $client->request('POST', '/en/my-account/payment/'.TestReference::UUID_404.'/prepare'); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + /** + * Nominal case: click an offer to initialize the payment process. In the test + * environment we use the "offline" gateway so the payment is always successful. + */ + public function testPrepareSuccess(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $crawler = $client->request('GET', self::ROUTE_SHOW_CHARGED); + $form = $crawler->selectButton('templates.pages.group.show.payment_prepare.form.submit')->form(); + $client->followRedirects(); + $client->submit($form); + self::assertResponseIsSuccessful(); + self::assertRouteSame('app_group_show'); + self::assertSelectorTextContains('body', 'flash.success'); + } +} diff --git a/tests/Functional/Controller/Product/ObjectControllerTest.php b/tests/Functional/Controller/Product/ObjectControllerTest.php new file mode 100644 index 0000000..50f8b46 --- /dev/null +++ b/tests/Functional/Controller/Product/ObjectControllerTest.php @@ -0,0 +1,209 @@ +loginAsUser16($client); + + $crawler = $client->request('GET', self::EDIT_USER_8_OBJECT_1_ROUTE); + self::assertResponseIsSuccessful(); + $form = $crawler->selectButton('object_form_submit')->form(); + + /** @var ChoiceFormField $categoryField */ + $categoryField = $form->get($form->getName().'[category]'); + $categoryField->disableValidation(); + + $crawler = $client->submit($form, [ + $form->getName().'[category]' => '', + $form->getName().'[name]' => '', + $form->getName().'[description]' => '', + ]); + + self::assertResponseIsUnprocessable(); + // 4 because there is the upload feedback div already in the response + self::assertSame(4, $crawler->filter('.invalid-feedback')->count()); + } + + public function testEditObjectForm(): void + { + $client = self::createClient(); + $this->loginAsUser16($client); + + $crawler = $client->request('GET', self::EDIT_USER_8_OBJECT_1_ROUTE); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton('object_form_submit')->form(); + + $client->submit($form, [ + $form->getName().'[category]' => TestReference::CATEGORY_OBJECT_1, + $form->getName().'[name]' => 'outils de poterie', + $form->getName().'[description]' => 'description test', + $form->getName().'[visibility]' => 'public', + $form->getName().'[age]' => '2012', + $form->getName().'[deposit]' => 5, + $form->getName().'[preferredLoanDuration]' => '1 journée', + ]); + + $repo = $this->getProductRepository(); + $objects = $repo->findBy(['owner' => TestReference::USER_16, 'type' => 'object']); + $editedObject = $repo->get(TestReference::SERVICE_USER_16_1); + + self::assertCount(TestReference::USER_8_OBJECTS_COUNT, $objects); + self::assertSame('outils de poterie', $editedObject->getName()); + self::assertSame('description test', $editedObject->getDescription()); + self::assertSame('2012', $editedObject->getAge()); + self::assertSame(500, $editedObject->getDeposit()); + self::assertTrue($editedObject->getVisibility()->isPublic()); + + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } + + public function testEditObjectFormWithTooLongName(): void + { + $client = self::createClient(); + $this->loginAsUser16($client); + + $crawler = $client->request('GET', self::EDIT_USER_8_OBJECT_1_ROUTE); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton('object_form_submit')->form(); + + $client->submit($form, [ + $form->getName().'[category]' => TestReference::CATEGORY_OBJECT_1, + $form->getName().'[name]' => str_repeat('Lorem ipsum', 100), + $form->getName().'[description]' => 'description test', + $form->getName().'[visibility]' => 'public', + $form->getName().'[age]' => '2012', + $form->getName().'[deposit]' => 5, + $form->getName().'[preferredLoanDuration]' => '1 journée', + ]); + + $repo = $this->getProductRepository(); + $objects = $repo->findBy(['owner' => TestReference::USER_16, 'type' => 'object']); + + self::assertCount(TestReference::USER_8_OBJECTS_COUNT, $objects); + self::assertResponseIsUnprocessable(); + self::assertSelectorTextContains('body', 'This value is too long.'); + } + + public function testNewObjectFormWithTooLongName(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + + $crawler = $client->request('GET', self::NEW_OBJECT_ROUTE); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton('object_form_submit')->form(); + $client->submit($form, [ + $form->getName().'[category]' => TestReference::CATEGORY_OBJECT_1, + $form->getName().'[name]' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam non odio libero. Nulla id fermentum augue, nec tempor mauris. In maximus magna malesuada velit molestie, et ultrices nulla lacinia. Vivamus mauris odio, commodo vel sapien vel, convallis ac..', + $form->getName().'[description]' => 'test description', + // $form->getName().'[visibility]' => 'public', + $form->getName().'[age]' => '2 ans', + $form->getName().'[deposit]' => 4, + ]); + + $repo = $this->getProductRepository(); + $objects = $repo->findBy(['owner' => TestReference::USER_16, 'type' => 'object']); + + self::assertResponseIsUnprocessable(); + self::assertSelectorTextContains('body', 'This value is too long.'); + self::assertCount(TestReference::USER_8_OBJECTS_COUNT, $objects); + } + + public function testNewObjectFormWithStringDepositValue(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + + $crawler = $client->request('GET', self::NEW_OBJECT_ROUTE); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton('object_form_submit')->form(); + + $client->submit($form, [ + $form->getName().'[category]' => TestReference::CATEGORY_OBJECT_1, + $form->getName().'[name]' => 'Diable', + $form->getName().'[description]' => 'test description', + $form->getName().'[age]' => '2 ans', + $form->getName().'[deposit]' => 'deux', + ]); + + $repo = $this->getProductRepository(); + $objects = $repo->findBy(['owner' => TestReference::USER_16, 'type' => 'object']); + self::assertResponseIsUnprocessable(); + self::assertSelectorTextContains('body', 'Please enter a valid money amount.'); + self::assertCount(TestReference::USER_8_OBJECTS_COUNT, $objects); + } + + public function testNewObjectFormSuccess(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + + $crawler = $client->request('GET', self::NEW_OBJECT_ROUTE); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton('object_form_submit')->form(); + + $client->submit($form, [ + $form->getName().'[category]' => TestReference::CATEGORY_OBJECT_1, + $form->getName().'[name]' => 'Diable', + $form->getName().'[description]' => 'test description', + // $form->getName().'[visibility]' => 'public', + $form->getName().'[age]' => '2 ans', + ]); + + $repo = $this->getProductRepository(); + $objects = $repo->findBy(['owner' => TestReference::USER_17, 'type' => 'object']); + + /** @var Product $newObject */ + $newObject = $repo->findOneBy(['name' => 'Diable']); + self::assertCount(TestReference::USER_17_SERVICES_COUNT + 1, $objects); + self::assertNull($newObject->getDepositReal()); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } + + public function testEditObjectFormForbidden(): void + { + $client = self::createClient(); + $this->loginAsUser11($client); + $client->request('GET', self::EDIT_USER_8_OBJECT_1_ROUTE); + self::assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN); + } +} diff --git a/tests/Functional/Controller/Product/ProductAvailabilityControllerTest.php b/tests/Functional/Controller/Product/ProductAvailabilityControllerTest.php new file mode 100644 index 0000000..e339143 --- /dev/null +++ b/tests/Functional/Controller/Product/ProductAvailabilityControllerTest.php @@ -0,0 +1,58 @@ +loginAsAdmin($client); + $client->request('GET', self::ROUTE_404); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + public function testFormSuccess(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $crawler = $client->request('GET', self::ROUTE); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(self::FORM_ID.'_submit')->form(); + $date = new \DateTimeImmutable('+ 1 day'); + $client->submit($form, [ + $form->getName().'[startAt]' => $date->format('Y-m-d'), + $form->getName().'[endAt]' => $date->modify('+1 day')->format('Y-m-d'), + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::FLASH_SUCCESS); + } +} diff --git a/tests/Functional/Controller/Product/ProductControllerTest.php b/tests/Functional/Controller/Product/ProductControllerTest.php new file mode 100644 index 0000000..fce7e4d --- /dev/null +++ b/tests/Functional/Controller/Product/ProductControllerTest.php @@ -0,0 +1,80 @@ +request('GET', self::ROUTE_LIST); + self::assertResponseIsSuccessful(); + } + + public function testListPaginationSuccess(): void + { + $client = self::createClient(); + + // 2nd valid page + $client->request('GET', self::ROUTE_LIST.'?page=2'); + self::assertResponseIsSuccessful(); + + // no error 500 on invalid page + $client->request('GET', self::ROUTE_LIST.'?page=5464646546542'); + self::assertResponseIsSuccessful(); + + // no error 500 on non numeric page + $client->request('GET', self::ROUTE_LIST.'?page=--fobarr'); + self::assertResponseIsSuccessful(); + } + + public function testShowNotFoundFailure(): void + { + $client = self::createClient(); + $client->request('GET', self::ROUTE_LIST.'/my-slug/'.TestReference::UUID_404); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + public function testShowSuccess(): void + { + $client = self::createClient(); + $client->request('GET', self::ROUTE_SHOW); + self::assertResponseIsSuccessful(); + } + + public function testShowLoggedCanBorrowSuccess(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + $client->request('GET', self::ROUTE_SHOW); + self::assertResponseIsSuccessful(); + } + + public function testShowLoggedCannotBorrowSuccess(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $client->request('GET', self::ROUTE_SHOW); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Product/ProductSearchControllerTest.php b/tests/Functional/Controller/Product/ProductSearchControllerTest.php new file mode 100644 index 0000000..060e131 --- /dev/null +++ b/tests/Functional/Controller/Product/ProductSearchControllerTest.php @@ -0,0 +1,67 @@ +request('GET', self::ROUTE_LIST.'?q=vélo'); + self::assertResponseIsSuccessful(); + } + + /** + * Advanced search. + */ + public function testSearchSuccess(): void + { + $client = self::createClient(); + $crawler = $client->request('GET', self::ROUTE_LIST); + self::assertResponseIsSuccessful(); + $form = $crawler->selectButton('p_submit')->form(); + $client->submit($form, [ + $form->getName().'[q]' => 'vélo', + $form->getName().'[category]' => TestReference::CATEGORY_OBJECT_2, + $form->getName().'[city]' => 'Lille', + $form->getName().'[distance]' => 5, + ]); + self::assertResponseIsSuccessful(); + } + + /** + * Place. + */ + public function testSearchPlaceSuccess(): void + { + $client = self::createClient(); + $crawler = $client->request('GET', self::ROUTE_LIST); + self::assertResponseIsSuccessful(); + $form = $crawler->selectButton('p_submit')->form(); + $client->submit($form, [ + $form->getName().'[place]' => TestReference::PLACE_APES, + ]); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Product/ServiceControllerTest.php b/tests/Functional/Controller/Product/ServiceControllerTest.php new file mode 100644 index 0000000..82e1148 --- /dev/null +++ b/tests/Functional/Controller/Product/ServiceControllerTest.php @@ -0,0 +1,134 @@ +loginAsUser16($client); + + $crawler = $client->request('GET', self::EDIT_USER_8_SERVICE_1_ROUTE); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton('service_form_submit')->form(); + + $client->submit($form, [ + $form->getName().'[category]' => TestReference::CATEGORY_SERVICE_1, + $form->getName().'[name]' => 'jardinage', + $form->getName().'[description]' => 'description', + $form->getName().'[duration]' => '1 jour', + $form->getName().'[visibility]' => 'restricted', + $form->getName().'[groups]' => [TestReference::GROUP_1], + ]); + + $container = $client->getContainer(); + $repo = $container->get(ProductRepository::class); + $services = $repo->findBy(['owner' => TestReference::USER_16, 'type' => 'service']); + /** @var Product $editedService */ + $editedService = $repo->find('1edae186-1b1e-6da8-8b71-e114a7d26c2e'); + + self::assertCount(TestReference::USER_8_SERVICES_COUNT, $services); + self::assertSame('jardinage', $editedService->getName()); + self::assertSame('description', $editedService->getDescription()); + self::assertSame('1 jour', $editedService->getDuration()); + self::assertSame('restricted', $editedService->getVisibility()->value); + + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } + + public function testEditServiceFormWithTooLongName(): void + { + $client = self::createClient(); + $this->loginAsUser16($client); + $crawler = $client->request('GET', self::EDIT_USER_8_SERVICE_1_ROUTE); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton('service_form_submit')->form(); + $client->submit($form, [ + $form->getName().'[category]' => TestReference::CATEGORY_SERVICE_1, + $form->getName().'[name]' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam non odio libero. Nulla id fermentum augue, nec tempor mauris. In maximus magna malesuada velit molestie, et ultrices nulla lacinia. Vivamus mauris odio, commodo vel sapien vel, convallis ac..', + $form->getName().'[description]' => 'description', + $form->getName().'[duration]' => '1 jour', + ]); + + $container = $client->getContainer(); + $repo = $container->get(ProductRepository::class); + $services = $repo->findBy(['owner' => TestReference::USER_16, 'type' => 'service']); + + self::assertCount(TestReference::USER_8_SERVICES_COUNT, $services); + self::assertResponseStatusCodeSame(422); + self::assertSelectorTextContains('body', 'This value is too long.'); + } + + public function testNewServiceFormWithTooLongName(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + $crawler = $client->request('GET', self::NEW_SERVICE_ROUTE); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton('service_form_submit')->form(); + $client->submit($form, [ + $form->getName().'[category]' => TestReference::CATEGORY_SERVICE_1, + $form->getName().'[name]' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam non odio libero. Nulla id fermentum augue, nec tempor mauris. In maximus magna malesuada velit molestie, et ultrices nulla lacinia. Vivamus mauris odio, commodo vel sapien vel, convallis ac..', + $form->getName().'[description]' => 'test description', + $form->getName().'[duration]' => '2 jours', + ]); + + $container = $client->getContainer(); + $repo = $container->get(ProductRepository::class); + $services = $repo->findBy(['owner' => TestReference::USER_16, 'type' => 'service']); + + self::assertResponseStatusCodeSame(422); + self::assertSelectorTextContains('body', 'This value is too long.'); + self::assertCount(TestReference::USER_8_SERVICES_COUNT, $services); + } + + public function testNewServiceForm(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + $crawler = $client->request('GET', self::NEW_SERVICE_ROUTE); + self::assertResponseIsSuccessful(); + + $imageName = 'apes.png'; + $image = realpath(__DIR__.'/../../../Fixtures/images/'.$imageName); + $uploadedFile = new UploadedFile((string) $image, $imageName); + $form = $crawler->selectButton('service_form_submit')->form(); + $client->submit($form, [ + $form->getName().'[category]' => TestReference::CATEGORY_SERVICE_1, + $form->getName().'[name]' => 'Aide bricolage', + $form->getName().'[description]' => 'test description', + $form->getName().'[duration]' => '2 jours', + $form->getName().'[images]' => [$uploadedFile], + ]); + + $container = $client->getContainer(); + $repo = $container->get(ProductRepository::class); + $services = $repo->findBy(['owner' => TestReference::USER_17, 'type' => 'service']); + self::assertCount(TestReference::USER_17_SERVICES_COUNT + 1, $services); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Security/AccountCreateActionStep1Test.php b/tests/Functional/Controller/Security/AccountCreateActionStep1Test.php new file mode 100644 index 0000000..68290d0 --- /dev/null +++ b/tests/Functional/Controller/Security/AccountCreateActionStep1Test.php @@ -0,0 +1,107 @@ + + */ + public function provideFormSubmitValidationError(): iterable + { + // empty data + yield ['', TestReference::VALIDATION_ERROR_BLANK]; + + // Test that even with a different case, the validation returns an error + // and doesn't throw a 500 error because of Doctrine constraints. + yield [strtoupper(TestReference::USER_EMAIL), TestReference::VALIDATION_ERROR_ALREADY_USED]; + } + + /** + * @@dataProvider provideFormSubmitValidationError + */ + public function testFormSubmitValidationError(string $email, string $error): void + { + $client = self::createClient(); + $crawler = $client->request('GET', self::ROUTE_EN); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton('account_create_step1_form_submit')->form(); + $client->submit($form, [ + $form->getName().'[email]' => $email, + ]); + self::assertResponseIsUnprocessable(); + self::assertSelectorTextContains('body', $error); + } + + /** + * @return iterable + */ + public function provideFormShowSuccess(): iterable + { + yield ['fr', self::ROUTE_FR]; + yield ['en', self::ROUTE_EN]; + } + + /** + * Nominal case. + * + * @dataProvider provideFormShowSuccess + */ + public function testFormSubmitSuccess(string $locale, string $route): void + { + $client = self::createClient(); + $crawler = $client->request('GET', $route); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton('account_create_step1_form_submit')->form(); + $client->submit($form, [ + $form->getName().'[email]' => $locale.self::NEW_USER_EMAIL, + ]); + + // user was created + $container = $client->getContainer(); + /** @var UserRepository $userRepo */ + $userRepo = $container->get(UserRepository::class); + $user = $userRepo->findOneByEmail($locale.self::NEW_USER_EMAIL); + self::assertInstanceOf(User::class, $user); + self::assertNotEmpty($user->getConfirmationToken()); + self::assertNotEmpty($user->getConfirmationExpiresAt()); + + // an email was sent with the confirmation link + self::assertEmailCount(1); + $emailMessage = self::getMailerMessage(); + self::assertInstanceOf(RawMessage::class, $emailMessage); + self::assertEmailHtmlBodyContains($emailMessage, $user->getConfirmationToken()); + + // then continue and redirect + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Security/AccountCreateActionStep2PlaceTest.php b/tests/Functional/Controller/Security/AccountCreateActionStep2PlaceTest.php new file mode 100644 index 0000000..1988ca5 --- /dev/null +++ b/tests/Functional/Controller/Security/AccountCreateActionStep2PlaceTest.php @@ -0,0 +1,53 @@ +request('GET', self::ROUTE.TestReference::USER_13_CONFIRMATION_TOKEN); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', 'app.controller.security.account_create_controller.step2.user_confirmation_token_expired.warning'); + } + + public function testFormSubmitPlaceSuccess(): void + { + $client = self::createClient(); + $crawler = $client->request('GET', self::ROUTE.TestReference::USER_12_CONFIRMATION_TOKEN); + $form = $crawler->selectButton('account_create_step2_form_submit')->form(); + + $password = ByteString::fromRandom(13); + $client->submit($form, [ + $form->getName().'[type]' => 'place', + $form->getName().'[name]' => 'My Association', + $form->getName().'[plainPassword][first]' => $password, + $form->getName().'[plainPassword][second]' => $password, + $form->getName().'[gdpr]' => 1, + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Security/AccountCreateActionStep2UserInvitationTest.php b/tests/Functional/Controller/Security/AccountCreateActionStep2UserInvitationTest.php new file mode 100644 index 0000000..599b2b7 --- /dev/null +++ b/tests/Functional/Controller/Security/AccountCreateActionStep2UserInvitationTest.php @@ -0,0 +1,49 @@ +request('GET', self::ROUTE.TestReference::USER_14_CONFIRMATION_TOKEN); + $form = $crawler->selectButton('account_create_step2_form_submit')->form(); + + $password = ByteString::fromRandom(13); + $client->submit($form, [ + $form->getName().'[type]' => 'user', + $form->getName().'[firstname]' => 'Foo', + $form->getName().'[lastname]' => 'Bar', + $form->getName().'[plainPassword][first]' => $password, + $form->getName().'[plainPassword][second]' => $password, + $form->getName().'[gdpr]' => 1, + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertRouteSame('app_group_show_logged'); + self::assertSelectorTextContains('body', 'APES Hauts-de-France'); + } +} diff --git a/tests/Functional/Controller/Security/AccountCreateActionStep2UserTest.php b/tests/Functional/Controller/Security/AccountCreateActionStep2UserTest.php new file mode 100644 index 0000000..d5feb55 --- /dev/null +++ b/tests/Functional/Controller/Security/AccountCreateActionStep2UserTest.php @@ -0,0 +1,54 @@ +request('GET', self::ROUTE.'foobar'); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', 'app.controller.security.account_create_controller.step2.user_not_found.warning'); + } + + public function testFormSubmitUserSuccess(): void + { + $client = self::createClient(); + $crawler = $client->request('GET', self::ROUTE.TestReference::USER_12_CONFIRMATION_TOKEN); + $form = $crawler->selectButton('account_create_step2_form_submit')->form(); + + $password = ByteString::fromRandom(13); + $client->submit($form, [ + $form->getName().'[type]' => 'user', + $form->getName().'[firstname]' => 'Foo', + $form->getName().'[lastname]' => 'Bar', + $form->getName().'[plainPassword][first]' => $password, + $form->getName().'[plainPassword][second]' => $password, + $form->getName().'[gdpr]' => 1, + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/Security/LostPasswordActionTest.php b/tests/Functional/Controller/Security/LostPasswordActionTest.php new file mode 100644 index 0000000..34c8b5d --- /dev/null +++ b/tests/Functional/Controller/Security/LostPasswordActionTest.php @@ -0,0 +1,46 @@ +request('GET', self::ROUTE); + $form = $crawler->selectButton('lost_password_form_submit')->form(); + $client->submit($form, [ + 'lost_password_form[email]' => 'usernotfound@example.com', + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', 'lost_password.form.success'); + } + + public function testFormSubmitSuccess(): void + { + $client = self::createClient(); + $crawler = $client->request('GET', self::ROUTE); + $form = $crawler->selectButton('lost_password_form_submit')->form(); + $client->submit($form, [ + $form->getName().'[email]' => TestReference::USER_17_EMAIL, + ]); + self::assertResponseRedirects(); + } +} diff --git a/tests/Functional/Controller/Security/ResetPasswordActionTest.php b/tests/Functional/Controller/Security/ResetPasswordActionTest.php new file mode 100644 index 0000000..3f848df --- /dev/null +++ b/tests/Functional/Controller/Security/ResetPasswordActionTest.php @@ -0,0 +1,58 @@ +request('GET', self::ROUTE.'foobar'); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', 'reset_password.user_not_found.exception'); + } + + public function testUserLostPasswordTokenExpiredException(): void + { + $client = self::createClient(); + $client->request('GET', self::ROUTE.TestReference::USER_15_LOST_PASSWORD_TOKEN); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', 'reset_password.user_lostpassword_token_expired.exception'); + } + + public function testFormSubmitSucess(): void + { + $client = self::createClient(); + $crawler = $client->request('GET', self::ROUTE.TestReference::USER_14_LOST_PASSWORD_TOKEN); + $form = $crawler->selectButton('reset_password_form_submit')->form(); + + $newPassword = ByteString::fromRandom(13); // min=8 @see UserManager + $client->submit($form, [ + $form->getName().'[password][first]' => $newPassword, + $form->getName().'[password][second]' => $newPassword, + ]); + self::assertResponseRedirects(); + } +} diff --git a/tests/Functional/Controller/Security/SecurityControllerTest.php b/tests/Functional/Controller/Security/SecurityControllerTest.php new file mode 100644 index 0000000..5388c4e --- /dev/null +++ b/tests/Functional/Controller/Security/SecurityControllerTest.php @@ -0,0 +1,137 @@ +request('GET', '/login'); + self::assertResponseIsSuccessful(); + } + + /** + * @see SecurityController::logout() + */ + public function testLogout(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $client->request('GET', '/logout'); + self::assertResponseRedirects('http://localhost/login'); + } + + /** + * Test the login manually so we don't skip some code parts, like the checkPostAuth() + * function of UserChecker. In all other tests we can use the login() helpers. + */ + public function testAdminLoginSuccess(): void + { + $client = self::createClient(); + $crawler = $client->request('GET', '/login'); + $form = $crawler->selectButton('submit')->form(); + $client->followRedirects(); + $client->submit($form, [ + self::USERNAME_FIELD => TestReference::ADMIN_EMAIL, + self::PASSWORD_FIELD => TestReference::PASSWORD_FIXTURES, + ]); + self::assertResponseIsSuccessful(); + self::assertRouteSame('admin'); + } + + public function testUserLoginSuccess(): void + { + $client = self::createClient(); + $crawler = $client->request('GET', '/login'); + $form = $crawler->selectButton('submit')->form(); + $client->followRedirects(); + $client->submit($form, [ + self::USERNAME_FIELD => TestReference::USER_EMAIL, + self::PASSWORD_FIELD => TestReference::PASSWORD_FIXTURES, + ]); + + self::assertResponseIsSuccessful(); + self::assertRouteSame('app_user_my_account'); + } + + /** + * Test that a user with a disabled account can't login. + * + * @see UserEnabledChecker + */ + public function testLoginAccountDisabledException(): void + { + $client = self::createClient(); + $crawler = $client->request('GET', '/login'); + $form = $crawler->selectButton('submit')->form(); + $client->submit($form, [ + self::USERNAME_FIELD => 'user10@example.com', + self::PASSWORD_FIELD => TestReference::PASSWORD_FIXTURES, + ]); + self::assertResponseRedirects('http://localhost/login'); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', 'login.account_disabled_exception'); + } + + /** + * Test that a user with an unconfirmed email can't login. + * + * @see UserEmailConfirmedChecker + */ + public function testLoginAccountEmailNotConfirmedException(): void + { + $client = self::createClient(); + $crawler = $client->request('GET', '/login'); + $form = $crawler->selectButton('submit')->form(); + $client->submit($form, [ + self::USERNAME_FIELD => 'user13@example.com', + self::PASSWORD_FIELD => TestReference::PASSWORD_FIXTURES, + ]); + self::assertResponseRedirects('http://localhost/login'); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', 'login.account_email_not_confirmed_exceptio'); + } + + /** + * Admin cannot be accessed by anonymous users. + */ + public function testAdminSecured(): void + { + $client = self::createClient(); + $client->request('GET', '/admin'); + self::assertResponseRedirects('/login'); + } + + /** + * Admin cannot be accessed by standard users. + */ + public function testAdminSecuredDenied(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + $client->request('GET', '/admin'); + self::assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN); + } +} diff --git a/tests/Functional/Controller/User/Account/ChangeLoginActionTest.php b/tests/Functional/Controller/User/Account/ChangeLoginActionTest.php new file mode 100644 index 0000000..fbb098e --- /dev/null +++ b/tests/Functional/Controller/User/Account/ChangeLoginActionTest.php @@ -0,0 +1,53 @@ +loginAsUser($client); + $crawler = $client->request('GET', self::ROUTE.'/mon-email'); + self::assertResponseIsSuccessful(); + $form = $crawler->selectButton('change_login_form_submit')->form(); + $client->submit($form, [ + $form->getName().'[email][first]' => self::NEW_EMAIL, + $form->getName().'[email][second]' => self::NEW_EMAIL, + ]); + self::assertResponseRedirects(self::ROUTE); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', 'app.controller.user.account.change_login_action.flash.success'); + } + + public function testEmailAlreadyUsedWarning(): void + { + $client = self::createClient(); + $this->loginAsSarah($client); + $crawler = $client->request('GET', self::ROUTE.'/mon-email'); + self::assertResponseIsSuccessful(); + $form = $crawler->selectButton('change_login_form_submit')->form(); + $client->submit($form, [ + $form->getName().'[email][first]' => TestReference::ADMIN_EMAIL, + $form->getName().'[email][second]' => TestReference::ADMIN_EMAIL, + ]); + self::assertResponseIsUnprocessable(); + } +} diff --git a/tests/Functional/Controller/User/Account/ChangePasswordActionTest.php b/tests/Functional/Controller/User/Account/ChangePasswordActionTest.php new file mode 100644 index 0000000..d051066 --- /dev/null +++ b/tests/Functional/Controller/User/Account/ChangePasswordActionTest.php @@ -0,0 +1,98 @@ +loginAsUser($client); + + $crawler = $client->request('GET', self::ROUTE.'/mon-mot-de-passe'); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton('templates.pages.user.account.change_password.submit')->form(); + $client->submit($form, [ + $form->getName().'[oldPassword]' => TestReference::PASSWORD_FIXTURES, + $form->getName().'[plainPassword][first]' => 'password', + $form->getName().'[plainPassword][second]' => 'password', + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', 'app.controller.user.account.change_password_action.flash.success'); + } + + public function testChangePasswordWithWrongOldPassword(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + + $crawler = $client->request('GET', self::ROUTE.'/mon-mot-de-passe'); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton('templates.pages.user.account.change_password.submit')->form(); + $client->submit($form, [ + $form->getName().'[oldPassword]' => 'old', + $form->getName().'[plainPassword][first]' => 'password', + $form->getName().'[plainPassword][second]' => 'password', + ]); + + self::assertResponseIsUnprocessable(); + self::assertSelectorTextContains('body', 'This value should be the user\'s current password.'); + } + + public function testChangePasswordWithValuesNotMatching(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + + $crawler = $client->request('GET', self::ROUTE.'/mon-mot-de-passe'); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton('templates.pages.user.account.change_password.submit')->form(); + $client->submit($form, [ + $form->getName().'[oldPassword]' => TestReference::PASSWORD_FIXTURES, + $form->getName().'[plainPassword][first]' => 'password', + $form->getName().'[plainPassword][second]' => 'newpassword', + ]); + + self::assertResponseIsUnprocessable(); + self::assertSelectorTextContains('body', 'The values do not match.'); + } + + public function testChangePasswordWithBlankValue(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + + $crawler = $client->request('GET', self::ROUTE.'/mon-mot-de-passe'); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton('templates.pages.user.account.change_password.submit')->form(); + $client->submit($form, [ + $form->getName().'[oldPassword]' => TestReference::PASSWORD_FIXTURES, + $form->getName().'[plainPassword][first]' => '', + $form->getName().'[plainPassword][second]' => '', + ]); + + self::assertResponseIsUnprocessable(); + self::assertSelectorTextContains('body', 'This value should not be blank.'); + } +} diff --git a/tests/Functional/Controller/User/Account/DeleteUserAvatarActionTest.php b/tests/Functional/Controller/User/Account/DeleteUserAvatarActionTest.php new file mode 100644 index 0000000..4164778 --- /dev/null +++ b/tests/Functional/Controller/User/Account/DeleteUserAvatarActionTest.php @@ -0,0 +1,35 @@ +loginAsUser16($client); + $client->request('GET', self::EDIT_ROUTE); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::FLASH_SUCCESS); + } +} diff --git a/tests/Functional/Controller/User/Account/EditProfileActionTest.php b/tests/Functional/Controller/User/Account/EditProfileActionTest.php new file mode 100644 index 0000000..fcae901 --- /dev/null +++ b/tests/Functional/Controller/User/Account/EditProfileActionTest.php @@ -0,0 +1,108 @@ +loginAsUser16($client); + + $client->request('GET', self::ROUTE); + self::assertResponseIsSuccessful(); + + $imageName = 'apes.png'; + $image = realpath(__DIR__.'/../../../../Fixtures/images/'.$imageName); + $uploadedFile = new UploadedFile((string) $image, $imageName); + + $form = $client->getCrawler()->selectButton('templates.pages.user.account.edit_profile.submit')->form(); + $client->submit($form, [ + $form->getName().'[firstname]' => 'John', + $form->getName().'[lastname]' => 'Doe', + $form->getName().'[avatar]' => $uploadedFile, + $form->getName().'[category]' => TestReference::CATEGORY_OBJECT_1, + $form->getName().'[description]' => 'description test', + $form->getName().'[phone][country]' => 'FR', + $form->getName().'[phone][number]' => '', + $form->getName().'[smsNotifications]' => false, + ]); + + $container = $client->getContainer(); + $repo = $container->get(UserRepository::class); + + /** @var User $editedUser */ + $editedUser = $repo->find(TestReference::USER_16); + self::assertNull($editedUser->getPhoneNumber()); + + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } + + public function testPlaceProfileAction(): void + { + $client = self::createClient(); + $this->loginAsPlaceApes($client); + + $client->request('GET', self::ROUTE); + self::assertResponseIsSuccessful(); + + $imageName = 'apes.png'; + $image = realpath(__DIR__.'/../../../../Fixtures/images/'.$imageName); + $uploadedFile = new UploadedFile((string) $image, $imageName); + + $form = $client->getCrawler()->selectButton('templates.pages.user.account.edit_profile.submit')->form(); + $client->submit($form, [ + $form->getName().'[name]' => 'Groupe 1', + $form->getName().'[avatar]' => $uploadedFile, + $form->getName().'[schedule]' => 'du lundi au vendredi', + $form->getName().'[phone][country]' => 'FR', + $form->getName().'[phone][number]' => '06 10 10 10 10', + $form->getName().'[smsNotifications]' => true, + ]); + + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + } + + public function testPlaceProfileActionWithoutName(): void + { + $client = self::createClient(); + $this->loginAsPlaceApes($client); + + $client->request('GET', self::ROUTE); + self::assertResponseIsSuccessful(); + + $form = $client->getCrawler()->selectButton('templates.pages.user.account.edit_profile.submit')->form(); + $client->submit($form, [ + $form->getName().'[name]' => '', + $form->getName().'[schedule]' => 'du lundi au vendredi', + $form->getName().'[phone][number]' => '', + $form->getName().'[smsNotifications]' => true, + ]); + self::assertResponseIsUnprocessable(); + self::assertSelectorTextContains('body', 'account_create.name.empty.error'); + } +} diff --git a/tests/Functional/Controller/User/Account/ProfileActionTest.php b/tests/Functional/Controller/User/Account/ProfileActionTest.php new file mode 100644 index 0000000..b774cbc --- /dev/null +++ b/tests/Functional/Controller/User/Account/ProfileActionTest.php @@ -0,0 +1,48 @@ +loginAsUser16($client); + $client->request('GET', self::ROUTE.'/'.TestReference::UUID_404); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + public function testMemberWithoutAddress(): void + { + $client = self::createClient(); + $this->loginAsUser16($client); + $client->request('GET', self::ROUTE.'/'.TestReference::USER_11); + self::assertResponseIsSuccessful(); + } + + public function testMember(): void + { + $client = self::createClient(); + $this->loginAsUser16($client); + $client->request('GET', self::ROUTE.'/'.TestReference::ADMIN_SARAH); + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Functional/Controller/User/AddressControllerTest.php b/tests/Functional/Controller/User/AddressControllerTest.php new file mode 100644 index 0000000..983b6da --- /dev/null +++ b/tests/Functional/Controller/User/AddressControllerTest.php @@ -0,0 +1,158 @@ +loginAsUser($client); + + $crawler = $client->request('GET', self::ROUTE_STEP1); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(self::STEP1_FORM_ID.'_submit')->form(); + $client->submit($form); + self::assertResponseRedirects(); + $crawler = $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::STEP2_TEXT); + + // Submit step 2 without selecting an address + $form = $crawler->selectButton(self::STEP2_FORM_ID.'_submit')->form(); + + $client->submit($form); + self::assertResponseIsUnprocessable(); + self::assertSelectorTextContains('body', self::STEP2_ERROR); + } + + /** + * User who already have an associated address. + */ + public function testStepsFormSuccess(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + + $crawler = $client->request('GET', self::ROUTE_STEP1); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(self::STEP1_FORM_ID.'_submit')->form(); + $client->submit($form); + self::assertResponseRedirects(); + $crawler = $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::STEP2_TEXT); + + // step 2, select first address + $form = $crawler->selectButton(self::STEP2_FORM_ID.'_submit')->form(); + /** @var ChoiceFormField $field */ + $field = $form[$form->getName().'[addresses]']; + $field->select('0'); // first choice + + $client->submit($form); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::STEP2_FLASH); + } + + /** + * User without address yet. + */ + public function testStep1FormNoAddressSuccess(): void + { + $client = self::createClient(); + $this->loginAsUser16($client); + + $crawler = $client->request('GET', self::ROUTE_STEP1); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(self::STEP1_FORM_ID.'_submit')->form(); + $client->submit($form, [ + $form->getName().'[address]' => '82 rue Winston Churchill', + $form->getName().'[addressSupplement]' => '3ème étage', + $form->getName().'[postalCode]' => '59160', + $form->getName().'[locality]' => 'Lille', + ]); + self::assertResponseRedirects(); + $crawler = $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::STEP2_TEXT); + + // step 2, select first address + $form = $crawler->selectButton(self::STEP2_FORM_ID.'_submit')->form(); + /** @var ChoiceFormField $field */ + $field = $form[$form->getName().'[addresses]']; + $field->select('0'); // first choice + + $client->submit($form); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::STEP2_FLASH); + } + + /** + * No address is found with the user input. We stay on the step1 page. + */ + public function testStep1FormFailure(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + + $crawler = $client->request('GET', self::ROUTE_STEP1); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(self::STEP1_FORM_ID.'_submit')->form(); + $client->submit($form, [ + $form->getName().'[address]' => 'ez', + $form->getName().'[addressSupplement]' => '000', + $form->getName().'[postalCode]' => '000', + $form->getName().'[locality]' => '000', + ]); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', 'address.step1_action.no_address.warning'); + } + + /** + * Step2 direct access is forbidden. + */ + public function testStep2DirectAccessFailure(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + $client->request('GET', self::ROUTE_STEP2); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::STEP1_TEXT); + } +} diff --git a/tests/Functional/Controller/User/Group/UserGroupAcceptInvitationActionTest.php b/tests/Functional/Controller/User/Group/UserGroupAcceptInvitationActionTest.php new file mode 100644 index 0000000..d39808c --- /dev/null +++ b/tests/Functional/Controller/User/Group/UserGroupAcceptInvitationActionTest.php @@ -0,0 +1,63 @@ +loginAsUser11($client); + $client->request('POST', '/en/my-account/groups/'.TestReference::GROUP_PRIVATE.'/acceptInvitation'); + self::assertResponseIsUnprocessable(); + } + + /** + * Group not found. + */ + public function testAcceptInvitationNotFoundException(): void + { + $client = self::createClient(); + $this->loginAsUser11($client); + $client->request('POST', '/en/my-account/groups/'.TestReference::UUID_404.'/acceptInvitation'); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + /** + * Nominal case: accept the invitation by clicking on the link. + */ + public function testAcceptInvitationSuccess(): void + { + $client = self::createClient(); + $this->loginAsUser11($client); + $crawler = $client->request('GET', self::ROUTE_SHOW); + self::assertResponseIsSuccessful(); + $form = $crawler->selectButton('templates.pages.group.show.form.accept_invitation.submit')->form(); + $client->submit($form); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertRouteSame('app_group_show'); + } +} diff --git a/tests/Functional/Controller/User/Group/UserGroupControllerTest.php b/tests/Functional/Controller/User/Group/UserGroupControllerTest.php new file mode 100644 index 0000000..3926ebf --- /dev/null +++ b/tests/Functional/Controller/User/Group/UserGroupControllerTest.php @@ -0,0 +1,78 @@ +loginAsUser($client); + $client->request('POST', '/en/my-account/groups/'.TestReference::GROUP_5.'/join'); + self::assertResponseIsUnprocessable(); + } + + /** + * Group not found. + */ + public function testJoinNotFoundException(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + $client->request('POST', '/en/my-account/groups/'.TestReference::UUID_404.'/join'); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + /** + * Nominal case: join a public group with free access. + */ + public function testJoinSuccess(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $crawler = $client->request('GET', self::ROUTE_SHOW_FREE); + self::assertResponseIsSuccessful(); + $form = $crawler->selectButton('templates.pages.group.show.group_join.form.submit')->form(); + $client->submit($form); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertRouteSame('app_group_show'); + } + + /** + * Test list of user groups. + */ + public function testList(): void + { + $client = self::createClient(); + $this->loginAsSarah($client); + $client->request('GET', self::ROUTE_USER_LIST); + self::assertResponseIsSuccessful(); + self::assertSame(1, $client->getCrawler()->filter('.invitation-test')->count()); + self::assertSame(3, $client->getCrawler()->filter('.group-test')->count()); + } +} diff --git a/tests/Functional/Controller/User/Group/UserGroupQuitGroupActionTest.php b/tests/Functional/Controller/User/Group/UserGroupQuitGroupActionTest.php new file mode 100644 index 0000000..28adb92 --- /dev/null +++ b/tests/Functional/Controller/User/Group/UserGroupQuitGroupActionTest.php @@ -0,0 +1,62 @@ +loginAsAdmin($client); + + $crawler = $client->request('GET', self::ROUTE_SHOW); + self::assertResponseIsSuccessful(); + $form = $crawler->selectButton('templates.pages.group.show.form.quit_group.submit')->form(); + $client->submit($form); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', 'flash.success'); + self::assertRouteSame('app_group_show'); + } + + /** + * Quit a group where I have associated products. + */ + public function testQuitGroupWithProductsSuccess(): void + { + $client = self::createClient(); + $this->loginAsPlaceApes($client); + + $crawler = $client->request('GET', self::ROUTE_SHOW_GROUP_1); + self::assertResponseIsSuccessful(); + $form = $crawler->selectButton('templates.pages.group.show.form.quit_group.submit')->form(); + $client->submit($form); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', 'flash.success'); + self::assertRouteSame('app_group_show'); + } +} diff --git a/tests/Functional/Controller/User/Product/DeleteProductActionTest.php b/tests/Functional/Controller/User/Product/DeleteProductActionTest.php new file mode 100644 index 0000000..9b91a64 --- /dev/null +++ b/tests/Functional/Controller/User/Product/DeleteProductActionTest.php @@ -0,0 +1,66 @@ +loginAsUser16($client); + $client->request('GET', self::DELETE_ROUTE_404); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + /** + * Access denied to other users. + */ + public function testAccessDeniedException(): void + { + $client = self::createClient(); + $this->loginAsUser16($client); + $client->request('GET', self::DELETE_ROUTE); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + /** + * Nominal case (owner). + */ + public function testDeleteProductSuccess(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $client->request('GET', self::EDIT_ROUTE); + self::assertResponseIsSuccessful(); + + $client->request('GET', self::DELETE_ROUTE); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::FLASH_SUCCESS); + } +} diff --git a/tests/Functional/Controller/User/Product/DeleteProductPhotoActionTest.php b/tests/Functional/Controller/User/Product/DeleteProductPhotoActionTest.php new file mode 100644 index 0000000..ed02852 --- /dev/null +++ b/tests/Functional/Controller/User/Product/DeleteProductPhotoActionTest.php @@ -0,0 +1,67 @@ +loginAsUser16($client); + $client->request('GET', self::DELETE_PHOTO_ROUTE_404); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + /** + * Access denied to other users. + */ + public function testAccessDeniedException(): void + { + $client = self::createClient(); + $this->loginAsUser16($client); + $client->request('GET', self::DELETE_PHOTO_ROUTE); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + /** + * Nominal case (owner). + */ + public function testOwnerSuccess(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $client->request('GET', self::EDIT_ROUTE); + self::assertResponseIsSuccessful(); + + // @todo: use POST+csrf token + $client->request('GET', self::DELETE_PHOTO_ROUTE); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::FLASH_SUCCESS); + } +} diff --git a/tests/Functional/Controller/User/Product/DeleteProductUnavailabilityActionTest.php b/tests/Functional/Controller/User/Product/DeleteProductUnavailabilityActionTest.php new file mode 100644 index 0000000..a7c4fb1 --- /dev/null +++ b/tests/Functional/Controller/User/Product/DeleteProductUnavailabilityActionTest.php @@ -0,0 +1,47 @@ +loginAsUser16($client); + $client->request('GET', self::ROUTE_OK); + self::assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN); + } + + public function testDeleteSuccess(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $client->request('GET', self::ROUTE_OK); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::FLASH_SUCCESS); + } +} diff --git a/tests/Functional/Controller/User/Product/DuplicateProductActionTest.php b/tests/Functional/Controller/User/Product/DuplicateProductActionTest.php new file mode 100644 index 0000000..0f987ed --- /dev/null +++ b/tests/Functional/Controller/User/Product/DuplicateProductActionTest.php @@ -0,0 +1,64 @@ +loginAsUser16($client); + $client->request('GET', self::ROUTE_404); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + /** + * Access denied to other users. + */ + public function testAccessDeniedException(): void + { + $client = self::createClient(); + $this->loginAsUser16($client); + $client->request('GET', self::ROUTE_OK); + self::assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN); + } + + /** + * Nominal case (owner). + */ + public function testFormOwnerSuccess(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $client->request('GET', self::ROUTE_OK); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::FLASH_SUCCESS); + } +} diff --git a/tests/Functional/Controller/User/ServiceRequest/ConversationControllerTest.php b/tests/Functional/Controller/User/ServiceRequest/ConversationControllerTest.php new file mode 100644 index 0000000..d239b0f --- /dev/null +++ b/tests/Functional/Controller/User/ServiceRequest/ConversationControllerTest.php @@ -0,0 +1,90 @@ +loginAsUser($client); + $client->request('GET', self::ROUTE_404); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + /** + * Access denied to other users. + */ + public function testAccessDeniedException(): void + { + $client = self::createClient(); + $this->loginAsUser16($client); + $client->request('GET', self::ROUTE_OK); + self::assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN); + } + + /** + * Nominal case (owner). + */ + public function testFormOwnerSuccess(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + $crawler = $client->request('GET', self::ROUTE_OK); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(self::FORM_ID.'_submit')->form(); + $client->submit($form, [ + $form->getName().'[message]' => 'Oui bien sûr ! Je regarde pour la date et je vous confirme ça.', + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::FLASH_SUCCESS); + } + + /** + * Nominal case (recipient). + */ + public function testFormRecipientSuccess(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $crawler = $client->request('GET', self::ROUTE_OK); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(self::FORM_ID.'_submit')->form(); + $client->submit($form, [ + $form->getName().'[message]' => "Ok merci. J'attends donc votre confirmation..", + ]); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::FLASH_SUCCESS); + } +} diff --git a/tests/Functional/Controller/User/ServiceRequest/MyLendingsActionTest.php b/tests/Functional/Controller/User/ServiceRequest/MyLendingsActionTest.php new file mode 100644 index 0000000..7612174 --- /dev/null +++ b/tests/Functional/Controller/User/ServiceRequest/MyLendingsActionTest.php @@ -0,0 +1,40 @@ +loginAsAdmin($client); + $client->request('GET', self::ROUTE); + self::assertResponseIsSuccessful(); + self::assertSame(3, $client->getCrawler()->filter('.conversation-test')->count()); + } + + public function testListByProduct(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $client->request('GET', self::ROUTE.'?user_lending_product_select_form[product][]='.TestReference::OBJECT_LOIC_1); + self::assertResponseIsSuccessful(); + + self::assertSame(2, $client->getCrawler()->filter('.conversation-test')->count()); + } +} diff --git a/tests/Functional/Controller/User/ServiceRequest/MyLoansActionTest.php b/tests/Functional/Controller/User/ServiceRequest/MyLoansActionTest.php new file mode 100644 index 0000000..c434d49 --- /dev/null +++ b/tests/Functional/Controller/User/ServiceRequest/MyLoansActionTest.php @@ -0,0 +1,40 @@ +loginAsAdmin($client); + $client->request('GET', self::ROUTE); + self::assertResponseIsSuccessful(); + self::assertSame(2, $client->getCrawler()->filter('.conversation-test')->count()); + } + + public function testListByProduct(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $client->request('GET', self::ROUTE.'?user_loans_product_select_form[product][]='.TestReference::OBJECT_KEVIN_1); + self::assertResponseIsSuccessful(); + + self::assertSame(1, $client->getCrawler()->filter('.conversation-test')->count()); + } +} diff --git a/tests/Functional/Controller/User/ServiceRequest/ServiceRequestControllerTest.php b/tests/Functional/Controller/User/ServiceRequest/ServiceRequestControllerTest.php new file mode 100644 index 0000000..227be66 --- /dev/null +++ b/tests/Functional/Controller/User/ServiceRequest/ServiceRequestControllerTest.php @@ -0,0 +1,107 @@ +loginAsUser($client); + $client->request('GET', self::ROUTE_404); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + /** + * Nominal case. + */ + public function testFormSuccess(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + $crawler = $client->request('GET', self::ROUTE_OK); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(self::FORM_ID.'_submit')->form(); + $date = new \DateTimeImmutable('+ 1 month'); + $client->submit($form, [ + $form->getName().'[message]' => 'Bonjour. Je voudrais emprunter votre superbe vélo Fuji Jari 2.5. 🙂', + $form->getName().'[startAt]' => $date->format('Y-m-d'), + $form->getName().'[endAt]' => $date->modify('+1 week')->format('Y-m-d'), + ]); + self::assertEmailCount(1); + self::assertNotificationCount(1); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::FLASH_SUCCESS); + } + + /** + * Wrong start and end dates passed as get arguments, they are simply ignored. + */ + public function testFormWrongDateSuccess(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + $client->request('GET', self::ROUTE_OK.'?startAt=foo&endAt=bar'); + self::assertResponseIsSuccessful(); + } + + /** + * Invalid uuid, we should have a 404, not a 500. + */ + public function testFormNotFoundOnInvalidUuid(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + $client->request('GET', self::ROUTE_OK.'-nokuuid'); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + /** + * Validation error: ProductAvailabilityNoOverlapValidator. In the fixtures, + * this object already has an ongoing loan that starts tomorrow (relative date). + * + * @see ProductAvailabilityNoOverlapValidator + */ + public function testFormProductAvailabilityNoOverlapValidationError(): void + { + $client = self::createClient(); + $this->loginAsUser($client); + $crawler = $client->request('GET', self::ROUTE_OK); + self::assertResponseIsSuccessful(); + + $form = $crawler->selectButton(self::FORM_ID.'_submit')->form(); + $date = new \DateTimeImmutable('today'); + $client->submit($form, [ + $form->getName().'[startAt]' => $date->format('Y-m-d'), + $form->getName().'[endAt]' => $date->modify('+1 week')->format('Y-m-d'), + ]); + self::assertResponseIsUnprocessable(); + self::assertSelectorTextContains('body', 'validator.product.productavailabilitynooverlap'); + } +} diff --git a/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowControllerAutoFinalizeTest.php b/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowControllerAutoFinalizeTest.php new file mode 100644 index 0000000..7dc4337 --- /dev/null +++ b/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowControllerAutoFinalizeTest.php @@ -0,0 +1,38 @@ +loginAsAdmin($client); + $crawler = $client->request('GET', self::ROUTE_OK); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', 'message.system.finalized'); + } +} diff --git a/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowControllerFinalizeTest.php b/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowControllerFinalizeTest.php new file mode 100644 index 0000000..102a65f --- /dev/null +++ b/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowControllerFinalizeTest.php @@ -0,0 +1,46 @@ +loginAsKevin($client); + $crawler = $client->request('GET', self::ROUTE_OK); + $form = $crawler->selectButton('templates.pages.account.conversation.link.finalize')->form(); + $client->submit($form); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::FLASH_SUCCESS.'.object.finalize'); + + // can't apply the same transition once again + $client->submit($form); + self::assertResponseIsUnprocessable(); + } +} diff --git a/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowControllerRefuseTest.php b/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowControllerRefuseTest.php new file mode 100644 index 0000000..0865c3c --- /dev/null +++ b/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowControllerRefuseTest.php @@ -0,0 +1,62 @@ +loginAsAdmin($client); + $crawler = $client->request('GET', self::ROUTE.TestReference::SERVICE_REQUEST_1.'/conversation'); + $form = $crawler->selectButton('templates.pages.account.conversation.link.refuse')->form(); + $client->submit($form); + self::assertEmailCount(1); + self::assertNotificationCount(1); + + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::FLASH_SUCCESS.'.object.refuse'); + } + + /** + * Nominal workflow: confirm. + */ + public function testTransitionRefuseOnConfirmStatusSuccess(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $crawler = $client->request('GET', self::ROUTE.TestReference::SERVICE_REQUEST_2.'/conversation'); + $form = $crawler->selectButton('templates.pages.account.conversation.link.refuse')->form(); + $client->submit($form); + self::assertEmailCount(1); + self::assertNotificationCount(1); + + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::FLASH_SUCCESS.'.object.refuse'); + } +} diff --git a/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowControllerTest.php b/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowControllerTest.php new file mode 100644 index 0000000..f3d2beb --- /dev/null +++ b/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowControllerTest.php @@ -0,0 +1,84 @@ +loginAsAdmin($client); + $client->request('POST', self::ROUTE.TestReference::SERVICE_REQUEST_1.'/transition/accept'); + self::assertEmailCount(0); + self::assertResponseIsUnprocessable(); + } + + /** + * Method not allowed (post with CSRF is needed). + */ + public function testTransitionLogicMethodNotAllowedException(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $client->request('GET', self::ROUTE_OK.'/finalize'); + self::assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND); + } + + /** + * Nominal workflow: accept, confirm. + */ + public function testTransitionsSuccess(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + + // 1. accept (owner) + $crawler = $client->request('GET', self::ROUTE_OK); + $form = $crawler->selectButton('templates.pages.account.conversation.link.confirm')->form(); + $client->submit($form); + self::assertEmailCount(1); + self::assertNotificationCount(1); + + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::FLASH_SUCCESS.'.object.accept'); + + // 2. confirm (recipient) + $this->logout($client); + $this->loginAsUser($client); + $crawler = $client->request('GET', self::ROUTE_OK); + $form = $crawler->selectButton('templates.pages.account.conversation.link.confirm')->form(); + $client->submit($form); + self::assertEmailCount(1); + self::assertNotificationCount(1); + + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::FLASH_SUCCESS.'.object.confirm'); + } +} diff --git a/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowModifyOwnerTest.php b/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowModifyOwnerTest.php new file mode 100644 index 0000000..3493c4a --- /dev/null +++ b/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowModifyOwnerTest.php @@ -0,0 +1,72 @@ +loginAsAdmin($client); + + // 1. modify dates (owner) + $crawler = $client->request('GET', self::ROUTE_OK); + $form = $crawler->selectButton(self::MODIFY_CONFIRM_BUTTON)->form(); + $date = new \DateTimeImmutable('+ 2 weeks'); // @see fixtures product_availability_user_1 + $client->submit($form, [ + $form->getName().'[startAt]' => $date->format('Y-m-d'), + $form->getName().'[endAt]' => $date->modify('+1 week')->format('Y-m-d'), + ]); + self::assertEmailCount(0); + self::assertResponseIsUnprocessable(); + self::assertSelectorTextContains('body', 'validator.product.productavailabilitynooverlap'); + } + + /** + * Nominal case. + */ + public function testTransitionsSuccess(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + + // 1. modify dates (owner) + $crawler = $client->request('GET', self::ROUTE_OK); + $form = $crawler->selectButton(self::MODIFY_CONFIRM_BUTTON)->form(); + $date = new \DateTimeImmutable('+ 3 days'); + $client->submit($form, [ + $form->getName().'[startAt]' => $date->format('Y-m-d'), + $form->getName().'[endAt]' => $date->modify('+3 days')->format('Y-m-d'), + ]); + self::assertEmailCount(1); + self::assertNotificationCount(1); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::FLASH_SUCCESS.'.object.modify_owner'); + } +} diff --git a/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowModifyRecipientTest.php b/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowModifyRecipientTest.php new file mode 100644 index 0000000..ad00b53 --- /dev/null +++ b/tests/Functional/Controller/User/ServiceRequest/ServiceRequestStatusWorkflowModifyRecipientTest.php @@ -0,0 +1,46 @@ +loginAsAdmin($client); + + $crawler = $client->request('GET', self::ROUTE_OK); + $form = $crawler->selectButton(self::MODIFY_CONFIRM_BUTTON)->form(); + $date = new \DateTimeImmutable('+ 3 days'); + $client->submit($form, [ + $form->getName().'[startAt]' => $date->format('Y-m-d'), + $form->getName().'[endAt]' => $date->modify('+3 days')->format('Y-m-d'), + ]); + self::assertEmailCount(1); + self::assertNotificationCount(1); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', TestReference::SERVICE_REQUEST_WORKFLOW_FLASH_SUCCESS.'.object.modify_recipient'); + } +} diff --git a/tests/Functional/Controller/User/UserProductsControllerTest.php b/tests/Functional/Controller/User/UserProductsControllerTest.php new file mode 100644 index 0000000..cab6e2b --- /dev/null +++ b/tests/Functional/Controller/User/UserProductsControllerTest.php @@ -0,0 +1,65 @@ +loginAsSarah($client); + $crawler = $client->request('GET', self::ROUTE_LIST_SERVICES); + self::assertResponseIsSuccessful(); + self::assertSame(TestReference::SARAH_SERVICES_COUNT, $crawler->filter('[data-test-product]')->count()); + + $form = $crawler->selectButton('service_category_select_form_submit')->form(); + /** @var ChoiceFormField $field */ + $field = $form[$form->getName().'[category]']; + $field->select(TestReference::SUB_CATEGORY_SERVICE_1); + $crawler = $client->submit($form); + self::assertResponseIsSuccessful(); + self::assertSame(TestReference::SARAH_SERVICES_COUNT - 1, $crawler->filter('[data-test-product]')->count()); + } + + public function testUserObjects(): void + { + $client = self::createClient(); + $this->loginAsUser16($client); + $crawler = $client->request('GET', self::ROUTE_LIST_OBJECTS); + self::assertResponseIsSuccessful(); + self::assertSame(TestReference::USER_8_OBJECTS_COUNT, $crawler->filter('[data-test-product]')->count()); + } + + public function testUserObjectsFilterByCategorySuccess(): void + { + $client = self::createClient(); + $this->loginAsAdmin($client); + $crawler = $client->request('GET', self::ROUTE_LIST_OBJECTS); + self::assertResponseIsSuccessful(); + self::assertSame(TestReference::ADMIN_LOIC_OBJECTS_COUNT, $crawler->filter('[data-test-product]')->count()); + + $form = $crawler->selectButton('object_category_select_form_submit')->form(); + /** @var ChoiceFormField $field */ + $field = $form[$form->getName().'[category]']; + $field->select(TestReference::CATEGORY_OBJECT_2); + $crawler = $client->submit($form); + self::assertResponseIsSuccessful(); + self::assertSame(1, $crawler->filter('[data-test-product]')->count()); + } +} diff --git a/tests/Functional/Controller/User/VacationModeActionTest.php b/tests/Functional/Controller/User/VacationModeActionTest.php new file mode 100644 index 0000000..990dffe --- /dev/null +++ b/tests/Functional/Controller/User/VacationModeActionTest.php @@ -0,0 +1,32 @@ +loginAsAdmin($client); + $client->request('GET', self::ROUTE); + self::assertResponseRedirects(); + $client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertSelectorTextContains('body', self::FLASH_SUCCESS); + } +} diff --git a/tests/Integration/Command/EndMembershipCommandTest.php b/tests/Integration/Command/EndMembershipCommandTest.php new file mode 100644 index 0000000..08185b8 --- /dev/null +++ b/tests/Integration/Command/EndMembershipCommandTest.php @@ -0,0 +1,44 @@ +fixDoctrineBug($kernel->getContainer()); + + $application = new Application($kernel); + $command = $application->find(EndMembershipCommand::CMD); + $commandTester = new CommandTester($command); + $commandTester->execute([]); + $commandTester->assertCommandIsSuccessful(); + self::assertEmailCount(1); + self::assertNotificationCount(1); + $output = $commandTester->getDisplay(); + self::assertStringContainsString(sprintf('%d deletion', 1), $output); + + // already deleted + $commandTester->execute([]); + $commandTester->assertCommandIsSuccessful(); + self::assertEmailCount(1); // not +1 + self::assertNotificationCount(1); + $output = $commandTester->getDisplay(); + self::assertStringContainsString(sprintf('%d deletion', 0), $output); + } +} diff --git a/tests/Integration/Command/NotifyMembershipExpirationCommandTest.php b/tests/Integration/Command/NotifyMembershipExpirationCommandTest.php new file mode 100644 index 0000000..04d0045 --- /dev/null +++ b/tests/Integration/Command/NotifyMembershipExpirationCommandTest.php @@ -0,0 +1,46 @@ +find(NotifyMembershipExpirationCommand::CMD); + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'days' => 1, + ]); + $commandTester->assertCommandIsSuccessful(); + $output = $commandTester->getDisplay(); + self::assertStringContainsString(sprintf('%d notification', 1), $output); + self::assertStringContainsString('Groupe 1 of Camille', $output); + self::assertEmailCount(1); + self::assertNotificationCount(1); + + // in one week + $commandTester->execute([ + 'days' => 7, + ]); + $commandTester->assertCommandIsSuccessful(); + $output = $commandTester->getDisplay(); + self::assertStringContainsString(sprintf('%d notification', 1), $output); + self::assertStringContainsString('Groupe 7 of Sarah', $output); + self::assertEmailCount(2); // cumulative + self::assertNotificationCount(2); + } +} diff --git a/tests/Integration/Command/NotifyServiceRequestDatesCommandTest.php b/tests/Integration/Command/NotifyServiceRequestDatesCommandTest.php new file mode 100644 index 0000000..5f1de53 --- /dev/null +++ b/tests/Integration/Command/NotifyServiceRequestDatesCommandTest.php @@ -0,0 +1,48 @@ +find(NotifyServiceRequestDatesCommand::CMD); + $commandTester = new CommandTester($command); + + // start notification + $commandTester->execute([ + 'mode' => 'start', + ]); + $commandTester->assertCommandIsSuccessful(); + $output = $commandTester->getDisplay(); + self::assertStringContainsString(sprintf('%d notification(s)', 2), $output); // owner + recipient + self::assertStringContainsString('DONE', $output); + self::assertEmailCount(2); + self::assertNotificationCount(2); + + // end notification + $commandTester->execute([ + 'mode' => 'end', + ]); + $commandTester->assertCommandIsSuccessful(); + $output = $commandTester->getDisplay(); + self::assertStringContainsString(sprintf('%d notification(s)', 2), $output); + self::assertStringContainsString('DONE', $output); + self::assertEmailCount(4); // cumulative results + self::assertNotificationCount(4); + } +} diff --git a/tests/Integration/DataFixtures/Processor/ValidationProcessorTest.php b/tests/Integration/DataFixtures/Processor/ValidationProcessorTest.php new file mode 100644 index 0000000..2dfd38f --- /dev/null +++ b/tests/Integration/DataFixtures/Processor/ValidationProcessorTest.php @@ -0,0 +1,25 @@ +getValidationProcessor(); + $group = new Group(); // a group must have a name + $this->expectException(\DomainException::class); + $this->expectExceptionMessageMatches('/Error when validating fixture \"my_wrong_fixture\"/'); + $this->expectExceptionMessageMatches('/This value should not be blank/'); + $validationProcessor->preProcess('my_wrong_fixture', $group); + } +} diff --git a/tests/Integration/Doctrine/Manager/ProductManagerTest.php b/tests/Integration/Doctrine/Manager/ProductManagerTest.php new file mode 100644 index 0000000..1099c88 --- /dev/null +++ b/tests/Integration/Doctrine/Manager/ProductManagerTest.php @@ -0,0 +1,30 @@ +getProductManager(); + $group = $this->getGroupRepository()->get(TestReference::GROUP_1); + $group2 = $this->getGroupRepository()->get(TestReference::GROUP_7); + $user = $this->getUserRepository()->get(TestReference::PLACE_APES); + $product = $this->getProductRepository()->get(TestReference::OBJECT_PLACE_6); + $product->addGroup($group2); + self::assertFalse($productManager->hasProductsOnlyInGroup($group, $user)); + } +} diff --git a/tests/Integration/Doctrine/UserManagerTest.php b/tests/Integration/Doctrine/UserManagerTest.php new file mode 100644 index 0000000..86cb1ee --- /dev/null +++ b/tests/Integration/Doctrine/UserManagerTest.php @@ -0,0 +1,39 @@ +getUserManager(); + $userRepo = $this->getUserRepository(); + + $user = new User(); + $user->setEmail(ByteString::fromRandom(6)->toString().'@example.com'); + $user->setPassword('foo'); + + $userManager->save($user, true); + $count = $userRepo->count([]); + self::assertSame(self::COUNT + 1, $count); + + $userManager->remove($user, true); + $count = $userRepo->count([]); + self::assertSame(self::COUNT, $count); + } +} diff --git a/tests/Integration/Flysystem/EasyAdminHelperTest.php b/tests/Integration/Flysystem/EasyAdminHelperTest.php new file mode 100644 index 0000000..0aa309b --- /dev/null +++ b/tests/Integration/Flysystem/EasyAdminHelperTest.php @@ -0,0 +1,33 @@ +get('category.storage'); + $helper = new EasyAdminHelper(); + $imageName = 'apes.png'; + + $image = (string) realpath(__DIR__.'/../../Fixtures/images/'.$imageName); + + $storage->write($imageName, (string) file_get_contents($image)); + self::assertTrue($storage->fileExists($imageName)); + + $file = new File($image); + $callback = $helper->getUploadDeleteCallback($storage); + $callback($file); + + self::assertFalse($storage->fileExists($imageName)); + } +} diff --git a/tests/Integration/Helper/FileUploaderTest.php b/tests/Integration/Helper/FileUploaderTest.php new file mode 100644 index 0000000..b10ffac --- /dev/null +++ b/tests/Integration/Helper/FileUploaderTest.php @@ -0,0 +1,37 @@ +get('product.storage'); + $helper = new FileUploader(); + $imageName1 = 'apes.png'; + $imageName2 = 'apes.png'; + + $image1 = realpath(__DIR__.'/../../Fixtures/images/'.$imageName1); + $uploadedFile1 = new UploadedFile((string) $image1, $imageName1); + + $image2 = realpath(__DIR__.'/../../Fixtures/images/'.$imageName2); + $uploadedFile2 = new UploadedFile((string) $image2, $imageName2); + + $newFilesName = $helper->uploadImageArray($storage, [$uploadedFile1, $uploadedFile2]); + + self::assertTrue($storage->fileExists($newFilesName[0])); + self::assertTrue($storage->fileExists($newFilesName[1])); + } +} diff --git a/tests/Integration/Mailer/AppMailerTest.php b/tests/Integration/Mailer/AppMailerTest.php new file mode 100644 index 0000000..5ddaa48 --- /dev/null +++ b/tests/Integration/Mailer/AppMailerTest.php @@ -0,0 +1,21 @@ +get(AppMailer::class); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('No email found to process the App\Mailer\AppMailer email'); + $appMailer->send(AppMailer::class, []); + } +} diff --git a/tests/Integration/MessageHandler/Payment/DoneCommandHandlerTest.php b/tests/Integration/MessageHandler/Payment/DoneCommandHandlerTest.php new file mode 100644 index 0000000..46f0dc0 --- /dev/null +++ b/tests/Integration/MessageHandler/Payment/DoneCommandHandlerTest.php @@ -0,0 +1,79 @@ +get(DoneCommandHandler::class); + self::assertInstanceOf(DoneCommandHandler::class, $handler); + + $groupOffer = $this->getGroupOfferRepository()->get(TestReference::GROUP_OFFER_GROUP_1_1); + $user = $this->getUserRepository()->get(TestReference::ADMIN_LOIC); + + $message = new DoneCommand($groupOffer->getId(), $user->getId(), $this->getToken($user)); + $status = $handler($message); + self::assertTrue($status->isNew()); + } + + private function getToken(User $user): PaymentToken + { + $token = new PaymentToken(); + $token->setGatewayName('offline'); + $payment = new Payment(); + $payment->setUser($user); + $token->setDetails($payment); + + return $token; + } + + /** + * No error 500 if the user is already a member. + */ + public function testDoneAlreadyMember(): void + { + self::bootKernel(); + $handler = self::getContainer()->get(DoneCommandHandler::class); + self::assertInstanceOf(DoneCommandHandler::class, $handler); + + $groupOffer = $this->getGroupOfferRepository()->get(TestReference::GROUP_OFFER_GROUP_1_1); + $user = $this->getUserRepository()->get(TestReference::ADMIN_LOIC); + $payment = $this->getPaymentRepository()->get(TestReference::PAYMENT_USER_16_1); + $token = new PaymentToken(); + $token->setGatewayName('offline'); + $payment->setUser($user); + $token->setDetails($payment); + + // meanwhile add the membership to test this specific case + $userGroup = (new UserGroup()) + ->setUser($user) + ->setGroup($groupOffer->getGroup()); + $user->addUserGroup($userGroup); + $this->getUserRepository()->save($user); + + $message = new DoneCommand($groupOffer->getId(), $user->getId(), $token); + $status = $handler($message); + self::assertTrue($status->isCaptured()); + } +} diff --git a/tests/Integration/MessageHandler/Security/AccountCreateStep2CommandHandlerTest.php b/tests/Integration/MessageHandler/Security/AccountCreateStep2CommandHandlerTest.php new file mode 100644 index 0000000..eca8563 --- /dev/null +++ b/tests/Integration/MessageHandler/Security/AccountCreateStep2CommandHandlerTest.php @@ -0,0 +1,36 @@ +get(AccountCreateStep2CommandHandler::class); + self::assertInstanceOf(AccountCreateStep2CommandHandler::class, $handler); + + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('This hanlder can only create users or places'); + $user = (new User()) + ->setId(Uuid::fromString(TestReference::USER_17)) + ->setType(UserType::ADMIN) + ->setPlainPassword('foo') + ; + $message = new AccountCreateStep2Command($user); + $handler($message); + } +} diff --git a/tests/Integration/MessageHandler/User/Group/AcceptGroupInvitationCommandHandlerTest.php b/tests/Integration/MessageHandler/User/Group/AcceptGroupInvitationCommandHandlerTest.php new file mode 100644 index 0000000..887d58b --- /dev/null +++ b/tests/Integration/MessageHandler/User/Group/AcceptGroupInvitationCommandHandlerTest.php @@ -0,0 +1,32 @@ +get(AcceptGroupInvitationCommandHandler::class); + self::assertInstanceOf(AcceptGroupInvitationCommandHandler::class, $handler); + $this->expectException(UnprocessableEntityHttpException::class); + + $group = $this->getGroupRepository()->get(TestReference::GROUP_PRIVATE); + $user = $this->getUserRepository()->get(TestReference::ADMIN_LOIC); + $message = new AcceptGroupInvitationCommand($group->getId(), $user->getId()); + $handler($message); + } +} diff --git a/tests/Integration/MessageHandler/User/Group/JoinGroupCommandHandlerTest.php b/tests/Integration/MessageHandler/User/Group/JoinGroupCommandHandlerTest.php new file mode 100644 index 0000000..a225e27 --- /dev/null +++ b/tests/Integration/MessageHandler/User/Group/JoinGroupCommandHandlerTest.php @@ -0,0 +1,82 @@ +get(JoinGroupCommandHandler::class); + self::assertInstanceOf(JoinGroupCommandHandler::class, $handler); + + $this->expectException(AccessDeniedHttpException::class); + $this->expectExceptionMessage('Group is not public'); + + $group = $this->getGroupRepository()->get(TestReference::GROUP_PRIVATE); + $user = $this->getUserRepository()->get(TestReference::ADMIN_LOIC); + $message = new JoinGroupCommand($group->getId(), $user->getId()); + $handler($message); + } + + /** + * Can't join a paying group. + */ + public function testIsChargedGroupException(): void + { + self::bootKernel(); + $this->fixDoctrine(); + $handler = self::getContainer()->get(JoinGroupCommandHandler::class); + self::assertInstanceOf(JoinGroupCommandHandler::class, $handler); + $group = $this->getGroupRepository()->get(TestReference::GROUP_1); + $user = $this->getUserRepository()->get(TestReference::USER_11); + + $this->expectException(AccessDeniedHttpException::class); + $this->expectExceptionMessage('Group has paying offers, the user must pay'); + + $message = new JoinGroupCommand($group->getId(), $user->getId()); + $handler($message); + self::assertTrue($user->isMemberOf($group)); + } + + /** + * No Doctrine error if the member is already member of the group. + */ + public function testIsMemberOfNoError(): void + { + self::bootKernel(); + $this->fixDoctrine(); + $handler = self::getContainer()->get(JoinGroupCommandHandler::class); + self::assertInstanceOf(JoinGroupCommandHandler::class, $handler); + $group = $this->getGroupRepository()->get(TestReference::GROUP_5); + $user = $this->getUserRepository()->get(TestReference::ADMIN_LOIC); + + $userGroup = (new UserGroup()) + ->setGroup($group) + ->setUser($user) + ->setMember(); + $group->addUserGroup($userGroup); + $this->getUserGroupRepository()->save($userGroup, true); + + self::assertTrue($user->hasLink($group)); + self::assertTrue($user->isMemberOf($group)); + + $message = new JoinGroupCommand($group->getId(), $user->getId()); + $handler($message); + self::assertTrue($user->isMemberOf($group)); + } +} diff --git a/tests/Integration/MessageHandler/User/Group/QuitGroupCommandHandlerTest.php b/tests/Integration/MessageHandler/User/Group/QuitGroupCommandHandlerTest.php new file mode 100644 index 0000000..106b6be --- /dev/null +++ b/tests/Integration/MessageHandler/User/Group/QuitGroupCommandHandlerTest.php @@ -0,0 +1,45 @@ +fixDoctrine(); + $handler = self::getContainer()->get(QuitGroupCommandHandler::class); + self::assertInstanceOf(QuitGroupCommandHandler::class, $handler); + $this->expectException(UnprocessableEntityHttpException::class); + + $group = $this->getGroupRepository()->get(TestReference::GROUP_PRIVATE); + $user = $this->getUserRepository()->get(TestReference::ADMIN_LOIC); + $message = new QuitGroupCommand($group->getId(), $user->getId(), null); + $handler($message); + } + + public function testQuitGroupWithAssociatedProductsAndSetPublic(): void + { + self::bootKernel(); + $this->fixDoctrine(); + $handler = self::getContainer()->get(QuitGroupCommandHandler::class); + self::assertInstanceOf(QuitGroupCommandHandler::class, $handler); + $group = $this->getGroupRepository()->get(TestReference::GROUP_1); + $user = $this->getUserRepository()->get(TestReference::PLACE_APES); + $message = new QuitGroupCommand($group->getId(), $user->getId(), 'public'); + $handler($message); + } +} diff --git a/tests/Integration/MessageHandler/User/Product/DuplicateProductCommandHandlerTest.php b/tests/Integration/MessageHandler/User/Product/DuplicateProductCommandHandlerTest.php new file mode 100644 index 0000000..7bc5785 --- /dev/null +++ b/tests/Integration/MessageHandler/User/Product/DuplicateProductCommandHandlerTest.php @@ -0,0 +1,31 @@ +get(DuplicateProductCommandHandler::class); + self::assertInstanceOf(DuplicateProductCommandHandler::class, $handler); + $this->expectException(AccessDeniedException::class); + $message = new DuplicateProductCommand(Uuid::fromString(TestReference::SERVICE_USER_16_1), ProductVoter::DUPLICATE); + $handler($message); + } +} diff --git a/tests/Integration/MessageHandler/User/UserAddressQueryHandlerTest.php b/tests/Integration/MessageHandler/User/UserAddressQueryHandlerTest.php new file mode 100644 index 0000000..2023176 --- /dev/null +++ b/tests/Integration/MessageHandler/User/UserAddressQueryHandlerTest.php @@ -0,0 +1,37 @@ +getMockBuilder(GeoProvider::class) + ->disableOriginalConstructor() + ->getMock(); + $nominatimGeocoder->method('getAddressCollection') + ->willThrowException(new InvalidServerResponse()); + + $handler = new UserAddressQueryHandler($nominatimGeocoder); + $this->expectException(\RuntimeException::class); + + $address = (new Address()) + ->setAddress('foo') + ->setLocality('foo') + ->setPostalCode('foo') + ->setCountry('FR') + ; + + $handler(new UserAddressQuery($address)); + } +} diff --git a/tests/Integration/Notifier/SmsNotifierTest.php b/tests/Integration/Notifier/SmsNotifierTest.php new file mode 100644 index 0000000..2fdc2fb --- /dev/null +++ b/tests/Integration/Notifier/SmsNotifierTest.php @@ -0,0 +1,28 @@ +get(SmsNotifier::class); + $user = new User(); + $sentMessage = $notifier->notify($user, 'subject'); + self::assertNull($sentMessage); + + $user->setSmsNotifications(true) + ->setPhoneNumber('0610101010'); + + $sentMessage = $notifier->notify($user, 'subject'); + self::assertNull($sentMessage); + } +} diff --git a/tests/Integration/Repository/AddressRepositoryTest.php b/tests/Integration/Repository/AddressRepositoryTest.php new file mode 100644 index 0000000..681e9a6 --- /dev/null +++ b/tests/Integration/Repository/AddressRepositoryTest.php @@ -0,0 +1,27 @@ +getAddressRepository(); + + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + } +} diff --git a/tests/Integration/Repository/CategoryRepositoryTest.php b/tests/Integration/Repository/CategoryRepositoryTest.php new file mode 100644 index 0000000..4585466 --- /dev/null +++ b/tests/Integration/Repository/CategoryRepositoryTest.php @@ -0,0 +1,49 @@ +getCategoryRepository(); + + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + + $category = new Category(); + $category->setName('grp'); + $category->setType(ProductType::OBJECT); + $repo->save($category, true); + $count = $repo->count([]); + self::assertSame(self::COUNT + 1, $count); + + $repo->remove($category, true); + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + + $child = new Category(); + $child->setName('child'); + $child->setType(ProductType::OBJECT); + + $category->addChild($child); + self::assertTrue($category->getChildren()->contains($child)); + $category->removeChild($child); + self::assertFalse($category->getChildren()->contains($child)); + } +} diff --git a/tests/Integration/Repository/ConfigurationRepositoryTest.php b/tests/Integration/Repository/ConfigurationRepositoryTest.php new file mode 100644 index 0000000..a4c4de2 --- /dev/null +++ b/tests/Integration/Repository/ConfigurationRepositoryTest.php @@ -0,0 +1,49 @@ +getConfigurationRepository(); + + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + + $configuration = new Configuration(); + $configuration->setType(ConfigurationType::INSTANCE); + $repo->save($configuration, true); + $count = $repo->count([]); + self::assertSame(self::COUNT + 1, $count); + + $repo->remove($configuration, true); + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + + /** @var Configuration $fixtureCfg */ + $fixtureCfg = $repo->getInstanceConfiguration(); + $repo->remove($fixtureCfg, true); + $repo->getInstanceConfigurationOrCreate(); + $count = $repo->count([]); + self::assertSame(0, $count); + } +} diff --git a/tests/Integration/Repository/GroupOfferRepositoryTest.php b/tests/Integration/Repository/GroupOfferRepositoryTest.php new file mode 100644 index 0000000..67dbc76 --- /dev/null +++ b/tests/Integration/Repository/GroupOfferRepositoryTest.php @@ -0,0 +1,26 @@ +getGroupOfferRepository(); + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + } +} diff --git a/tests/Integration/Repository/GroupRepositoryTest.php b/tests/Integration/Repository/GroupRepositoryTest.php new file mode 100644 index 0000000..34d43f6 --- /dev/null +++ b/tests/Integration/Repository/GroupRepositoryTest.php @@ -0,0 +1,49 @@ +getGroupRepository(); + + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + + $group = new Group(); + $group->setName('grp'); + $repo->save($group, true); + $count = $repo->count([]); + self::assertSame(self::COUNT + 1, $count); + + $repo->remove($group, true); + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + + $child = new Group(); + $child->setName('child'); + + $group->addChild($child); + self::assertTrue($group->getChildren()->contains($child)); + $group->removeChild($child); + self::assertFalse($group->getChildren()->contains($child)); + } +} diff --git a/tests/Integration/Repository/MenuItemRepositoryTest.php b/tests/Integration/Repository/MenuItemRepositoryTest.php new file mode 100644 index 0000000..fa19507 --- /dev/null +++ b/tests/Integration/Repository/MenuItemRepositoryTest.php @@ -0,0 +1,39 @@ +getMenuItemRepository(); + + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + + $menuItems = new MenuItem(); + $menuItems->setName('name'); + $menuItems->setLink('link'); + $repo->save($menuItems, true); + $count = $repo->count([]); + self::assertSame(self::COUNT + 1, $count); + + $repo->remove($menuItems, true); + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + } +} diff --git a/tests/Integration/Repository/MenuRepositoryTest.php b/tests/Integration/Repository/MenuRepositoryTest.php new file mode 100644 index 0000000..655dc2e --- /dev/null +++ b/tests/Integration/Repository/MenuRepositoryTest.php @@ -0,0 +1,39 @@ +getMenuRepository(); + + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + + $menu = new Menu(); + $menu->setLogo('logo'); + $menu->setCode('menu_left'); + $repo->save($menu, true); + $count = $repo->count([]); + self::assertSame(self::COUNT + 1, $count); + + $repo->remove($menu, true); + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + } +} diff --git a/tests/Integration/Repository/MessageRepositoryTest.php b/tests/Integration/Repository/MessageRepositoryTest.php new file mode 100644 index 0000000..28d8c62 --- /dev/null +++ b/tests/Integration/Repository/MessageRepositoryTest.php @@ -0,0 +1,42 @@ +getMessageRepository(); + + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + + $message = new Message(); + $message->setServiceRequest($this->getServiceRequestRepository()->get(TestReference::SERVICE_REQUEST_1)); + $message->setType(MessageType::FROM_OWNER); + $message->setMessage('foobar'); + + $repo->save($message, true); + $count = $repo->count([]); + self::assertSame(self::COUNT + 1, $count); + + $repo->remove($message, true); + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + } +} diff --git a/tests/Integration/Repository/ProductAvailabilityRepositoryTest.php b/tests/Integration/Repository/ProductAvailabilityRepositoryTest.php new file mode 100644 index 0000000..3fd96b8 --- /dev/null +++ b/tests/Integration/Repository/ProductAvailabilityRepositoryTest.php @@ -0,0 +1,26 @@ +getProductAvailabilityRepository(); + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + } +} diff --git a/tests/Integration/Repository/ProductRepositoryTest.php b/tests/Integration/Repository/ProductRepositoryTest.php new file mode 100644 index 0000000..b92bf44 --- /dev/null +++ b/tests/Integration/Repository/ProductRepositoryTest.php @@ -0,0 +1,72 @@ +getProductRepository(); + + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + + $product = new Product(); + $product->setType(ProductType::OBJECT); + $product->setStatus(ProductStatus::ACTIVE); + $product->setVisibility(ProductVisibility::PUBLIC); + /** @var User $user */ + $user = $this->getUserRepository()->find(TestReference::ADMIN_LOIC); + $product->setOwner($user); + + /** @var Category $category */ + $category = $this->getCategoryRepository()->find(TestReference::CATEGORY_OBJECT_1); + + $product->setCategory($category); + $product->setName('prd'); + $product->setAge('récent'); + + $repo->save($product, true); + $count = $repo->count([]); + self::assertSame(self::COUNT + 1, $count); + + $repo->remove($product, true); + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + } + + public function testDeleteProduct(): void + { + $repo = $this->getProductRepository(); + self::assertSame(self::COUNT, $repo->count([])); + $product = $repo->get(TestReference::OBJECT_LOIC_2); + $repo->remove($product, true); + self::assertSame(self::COUNT - 1, $repo->count([])); + } + + public function testGetProducts(): void + { + $repo = $this->getProductRepository(); + self::assertNotEmpty($repo->getObjects()->getArrayResult()); + self::assertNotEmpty($repo->getServices()->getArrayResult()); + } +} diff --git a/tests/Integration/Repository/ServiceRequestRepositoryTest.php b/tests/Integration/Repository/ServiceRequestRepositoryTest.php new file mode 100644 index 0000000..387936f --- /dev/null +++ b/tests/Integration/Repository/ServiceRequestRepositoryTest.php @@ -0,0 +1,47 @@ +getServiceRequestRepository(); + + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + + $loic = $this->getUserRepository()->get(TestReference::ADMIN_LOIC); + $camille = $this->getUserRepository()->get(TestReference::ADMIN_CAMILLE); + $product = $this->getProductRepository()->get(TestReference::OBJECT_LOIC_1); + + $serviceRequest = new ServiceRequest(); + $serviceRequest->setProduct($product); + $serviceRequest->setOwner($loic); + $serviceRequest->setRecipient($camille); + $serviceRequest->setStartAt(new \DateTimeImmutable('tomorrow')); + $serviceRequest->setEndAt(new \DateTimeImmutable('+1 week')); + + $repo->save($serviceRequest, true); + $count = $repo->count([]); + self::assertSame(self::COUNT + 1, $count); + + $repo->remove($serviceRequest, true); + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + } +} diff --git a/tests/Integration/Repository/UserGroupRepositoryTest.php b/tests/Integration/Repository/UserGroupRepositoryTest.php new file mode 100644 index 0000000..3b1b046 --- /dev/null +++ b/tests/Integration/Repository/UserGroupRepositoryTest.php @@ -0,0 +1,30 @@ +getUserGroupRepository(); + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + + $userGroup = new UserGroup(); + $repo->remove($userGroup, true); + } +} diff --git a/tests/Integration/Repository/UserRepositoryTest.php b/tests/Integration/Repository/UserRepositoryTest.php new file mode 100644 index 0000000..841e347 --- /dev/null +++ b/tests/Integration/Repository/UserRepositoryTest.php @@ -0,0 +1,53 @@ +getUserRepository(); + + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + + $user = new User(); + $user->setEmail(ByteString::fromRandom(6)->toString().'@example.com'); + $user->setPassword('foo'); + $repo->save($user, true); + $count = $repo->count([]); + self::assertSame(self::COUNT + 1, $count); + + $repo->upgradePassword($user, 'foo'); + + $repo->remove($user, true); + $count = $repo->count([]); + self::assertSame(self::COUNT, $count); + } + + public function testUpgradePasswordException(): void + { + self::bootKernel(); + $repo = $this->getUserRepository(); + $user = $this->getMockBuilder(PasswordAuthenticatedUserInterface::class)->getMock(); + $this->expectException(UnsupportedUserException::class); + $repo->upgradePassword($user, 'foo'); + } +} diff --git a/tests/Integration/Search/Command/IndexProductsCommandTest.php b/tests/Integration/Search/Command/IndexProductsCommandTest.php new file mode 100644 index 0000000..ae155af --- /dev/null +++ b/tests/Integration/Search/Command/IndexProductsCommandTest.php @@ -0,0 +1,67 @@ +find(IndexProductsCommand::CMD); + $commandTester = new CommandTester($command); + $commandTester->execute([]); + $commandTester->assertCommandIsSuccessful(); + $output = $commandTester->getDisplay(); + self::assertStringContainsString(sprintf('%d product(s) indexed', TestReference::PRODUCTS_INDEXABLE_COUNT), $output); + + // check if the search is OK + // sleep(2); // wait a little to be sure the indexing process is OK + /** @var Meilisearch $meilisearch */ + $meilisearch = self::getContainer()->get(Meilisearch::class); + $client = $meilisearch->getClient(); + + // Wait for Meilisearch to be ready + $tasks = $client->getTasks(); + $uids = array_column($tasks->toArray(), 'uid'); + $client->waitForTasks($uids); + + $infos = $client->getIndex('products'); + self::assertSame('products', $infos->getUid()); + + $searchDto = new Search(''); + + // all documents when not logged (-1 because of a restricted product) + $results = $meilisearch->search($searchDto); + self::assertSame(TestReference::PRODUCTS_INDEXABLE_COUNT - 1, $results->getHitsCount()); + + // all documents when logged with a user with access to the restricted product + $searchDto->user = $this->getUserRepository()->get(TestReference::PLACE_APES); + $results = $meilisearch->search($searchDto); + self::assertSame(TestReference::PRODUCTS_INDEXABLE_COUNT, $results->getHitsCount()); + + // keyword search + $searchDto->user = null; + $searchDto->q = 'vélo'; + $results = $meilisearch->search($searchDto); + self::assertSame(3, $results->getHitsCount()); + + // typo tolerance example + $searchDto->q = 'histiore'; + $results = $meilisearch->search($searchDto); + self::assertSame(1, $results->getHitsCount()); + } +} diff --git a/tests/Integration/Search/MeilisearchTest.php b/tests/Integration/Search/MeilisearchTest.php new file mode 100644 index 0000000..b10ce0c --- /dev/null +++ b/tests/Integration/Search/MeilisearchTest.php @@ -0,0 +1,28 @@ +get(Meilisearch::class); + $object = $this->getProductRepository()->get(TestReference::OBJECT_LOIC_1); + $service = $this->getProductRepository()->get(TestReference::SERVICE_LOIC_1); + $meilisearch->indexProducts([$object, $service]); + $searchDto = new Search('vélo'); + $results = $meilisearch->search($searchDto); + self::assertNotEmpty($results->getHitsCount()); + } +} diff --git a/tests/Integration/Security/Checker/AuthorizationCheckerTest.php b/tests/Integration/Security/Checker/AuthorizationCheckerTest.php new file mode 100644 index 0000000..b5e3cac --- /dev/null +++ b/tests/Integration/Security/Checker/AuthorizationCheckerTest.php @@ -0,0 +1,21 @@ +get(AuthorizationChecker::class); + $this->expectException(AccessDeniedHttpException::class); + $service->isGroupAdmin(); + } +} diff --git a/tests/Integration/Translator/NotranslatorTest.php b/tests/Integration/Translator/NotranslatorTest.php new file mode 100644 index 0000000..be5259f --- /dev/null +++ b/tests/Integration/Translator/NotranslatorTest.php @@ -0,0 +1,22 @@ +get(NoTranslator::class); + self::assertSame('fr', $translator->getLocale()); + $translator->setLocale('en'); + self::assertSame('en', $translator->getLocale()); + self::assertSame([], $translator->getCatalogues()); + } +} diff --git a/tests/Integration/Twig/Extension/CategoryExtensionTest.php b/tests/Integration/Twig/Extension/CategoryExtensionTest.php new file mode 100644 index 0000000..9f03900 --- /dev/null +++ b/tests/Integration/Twig/Extension/CategoryExtensionTest.php @@ -0,0 +1,28 @@ +getCategoryExtension(); + $category = new Category(); + $name = 'apes.png'; + $category->setImage($name); + $publicUrl = $categoryExtension->getPublicUrl($category); + self::assertSame('/storage/uploads/category/apes.png', $publicUrl); + } +} diff --git a/tests/Integration/Twig/Extension/EntityExtensionTest.php b/tests/Integration/Twig/Extension/EntityExtensionTest.php new file mode 100644 index 0000000..5e4eb0e --- /dev/null +++ b/tests/Integration/Twig/Extension/EntityExtensionTest.php @@ -0,0 +1,30 @@ +getEntityExtension(); + self::assertFalse($categoryExtension->isImagesEntity(new Address())); + self::assertTrue($categoryExtension->isImagesEntity(new Product())); + + self::assertFalse($categoryExtension->isImageEntity(new Address())); + self::assertTrue($categoryExtension->isImageEntity(new Menu())); + } +} diff --git a/tests/Integration/Twig/Extension/FlysystemExtensionTest.php b/tests/Integration/Twig/Extension/FlysystemExtensionTest.php new file mode 100644 index 0000000..bdb4746 --- /dev/null +++ b/tests/Integration/Twig/Extension/FlysystemExtensionTest.php @@ -0,0 +1,58 @@ +getFlysystemExtension(); + $category = new Category(); + $name = 'apes.png'; + $category->setImage($name); + $publicUrl = $flysystemExtension->getPublicUrl($category); + self::assertSame('/storage/uploads/category/apes.png', $publicUrl); + + $user = (new User())->setAvatar($name); + $publicUrl = $flysystemExtension->getPublicUrl($user); + self::assertSame('/storage/uploads/user/apes.png', $publicUrl); + + $product = (new Product())->setImages([$name]); + $publicUrlImage = $flysystemExtension->getPublicUrlImage($product, $name); + self::assertSame('/storage/uploads/product/apes.png', $publicUrlImage); + } + + public function testGetPublicUrlException(): void + { + self::bootKernel(); + $flysystemExtension = $this->getFlysystemExtension(); + $dummyImage = new DummyImage(); + $this->expectException(\LogicException::class); + $flysystemExtension->getPublicUrl($dummyImage); + } + + public function testGetPublicUrlImageException(): void + { + self::bootKernel(); + $flysystemExtension = $this->getFlysystemExtension(); + $dummyImages = new DummyImages(); + $this->expectException(\LogicException::class); + $flysystemExtension->getPublicUrlImage($dummyImages, 'foo.png'); + } +} diff --git a/tests/Integration/Twig/Extension/I18nExtensionTest.php b/tests/Integration/Twig/Extension/I18nExtensionTest.php new file mode 100644 index 0000000..e1b8fa2 --- /dev/null +++ b/tests/Integration/Twig/Extension/I18nExtensionTest.php @@ -0,0 +1,21 @@ +getI18nExtension(); + // we use use trimSuffix() not trimEnd() in getI18Prefix(), otherwise the final "t" would be removed + self::assertSame('templates.pages.group.list', $extension->getI18Prefix('pages/group/list.html.twig')); + } +} diff --git a/tests/Integration/Twig/Extension/MenuExtensionTest.php b/tests/Integration/Twig/Extension/MenuExtensionTest.php new file mode 100644 index 0000000..ba7f424 --- /dev/null +++ b/tests/Integration/Twig/Extension/MenuExtensionTest.php @@ -0,0 +1,27 @@ +getMenuExtension(); + $menu = new Menu(); + $menu->setLogo('logo.png'); + $publicUrl = $menuExtension->getPublicUrl($menu); + self::assertSame('/storage/uploads/logo.png', $publicUrl); + } +} diff --git a/tests/Integration/Twig/Extension/ResponseExtensionTest.php b/tests/Integration/Twig/Extension/ResponseExtensionTest.php new file mode 100644 index 0000000..e01fab3 --- /dev/null +++ b/tests/Integration/Twig/Extension/ResponseExtensionTest.php @@ -0,0 +1,20 @@ +get(ResponseExtension::class); + self::assertNotEmpty($extension->getFilters()); + self::assertSame('Not Found', $extension->getStatusText(Response::HTTP_NOT_FOUND)); + } +} diff --git a/tests/Integration/Twig/Extension/TwigExtensionTest.php b/tests/Integration/Twig/Extension/TwigExtensionTest.php new file mode 100644 index 0000000..d7acb71 --- /dev/null +++ b/tests/Integration/Twig/Extension/TwigExtensionTest.php @@ -0,0 +1,20 @@ +getTwigExtension(); + self::assertSame('my_controler', $extension->snake('myControler')); + } +} diff --git a/tests/Integration/Twig/Extension/UserExtensionTest.php b/tests/Integration/Twig/Extension/UserExtensionTest.php new file mode 100644 index 0000000..dbcc9dd --- /dev/null +++ b/tests/Integration/Twig/Extension/UserExtensionTest.php @@ -0,0 +1,30 @@ +getUserExtension(); + $user = new User(); + $name = 'apes.png'; + $user->setAvatar($name); + $publicUrl = $userExtension->getPublicUrl($user); + self::assertSame('/storage/uploads/user/apes.png', $publicUrl); + } +} diff --git a/tests/Integration/Workflow/ServiceRequestStatusWorkflowTest.php b/tests/Integration/Workflow/ServiceRequestStatusWorkflowTest.php new file mode 100644 index 0000000..cca0961 --- /dev/null +++ b/tests/Integration/Workflow/ServiceRequestStatusWorkflowTest.php @@ -0,0 +1,203 @@ +get(ServiceRequestStatusWorkflow::class); + + return $serviceRequestStatusWorkflow; + } + + private function getServiceRequest(): ServiceRequest + { + $sr = $this->getServiceRequestRepository()->get(TestReference::SERVICE_REQUEST_1); + $sr->setStatus(ServiceRequestStatus::NEW); + + return $sr; + } + + public function testAcceptTransitionSuccess(): void + { + self::bootKernel(); + $serviceRequestStatusWorkflow = $this->getServiceRequestStatusWorkflow(); + $sr = $this->getServiceRequest(); + + self::assertTrue($serviceRequestStatusWorkflow->canAccept($sr)); + self::assertSame(ServiceRequestStatus::TO_CONFIRM, $serviceRequestStatusWorkflow->accept($sr)->getStatus()); + } + + public function testAcceptTransitionException(): void + { + self::bootKernel(); + $serviceRequestStatusWorkflow = $this->getServiceRequestStatusWorkflow(); + $sr = $this->getServiceRequest(); + + $sr->setStatus(ServiceRequestStatus::FINISHED); + self::assertFalse($serviceRequestStatusWorkflow->canAccept($sr)); + $this->expectException(\LogicException::class); + $serviceRequestStatusWorkflow->accept($sr); + } + + public function testModifyOwnerTransitionSuccess(): void + { + self::bootKernel(); + $serviceRequestStatusWorkflow = $this->getServiceRequestStatusWorkflow(); + $sr = $this->getServiceRequest(); + + self::assertTrue($serviceRequestStatusWorkflow->canModifyOwner($sr)); + self::assertSame(ServiceRequestStatus::TO_CONFIRM, $serviceRequestStatusWorkflow->modifyOwner($sr)->getStatus()); + } + + public function testModifyOwnerTransitionException(): void + { + self::bootKernel(); + $serviceRequestStatusWorkflow = $this->getServiceRequestStatusWorkflow(); + $sr = $this->getServiceRequest() + ->setStatus(ServiceRequestStatus::FINISHED); + self::assertFalse($serviceRequestStatusWorkflow->canModifyRecipient($sr)); + $this->expectException(\LogicException::class); + $serviceRequestStatusWorkflow->modifyRecipient($sr); + } + + public function testModifyRecipientTransitionSuccess(): void + { + self::bootKernel(); + $serviceRequestStatusWorkflow = $this->getServiceRequestStatusWorkflow(); + $sr = $this->getServiceRequest() + ->setStatus(ServiceRequestStatus::TO_CONFIRM); + + self::assertTrue($serviceRequestStatusWorkflow->canModifyRecipient($sr)); + self::assertSame(ServiceRequestStatus::NEW, $serviceRequestStatusWorkflow->modifyRecipient($sr)->getStatus()); + } + + public function testModifyRecipientTransitionException(): void + { + self::bootKernel(); + $serviceRequestStatusWorkflow = $this->getServiceRequestStatusWorkflow(); + $sr = $this->getServiceRequest(); + + $sr->setStatus(ServiceRequestStatus::FINISHED); + self::assertFalse($serviceRequestStatusWorkflow->canModifyRecipient($sr)); + $this->expectException(\LogicException::class); + $serviceRequestStatusWorkflow->modifyOwner($sr); + } + + public function testConfirmTransitionSuccess(): void + { + self::bootKernel(); + $serviceRequestStatusWorkflow = $this->getServiceRequestStatusWorkflow(); + $sr = $this->getServiceRequest() + ->setStatus(ServiceRequestStatus::TO_CONFIRM); + + self::assertTrue($serviceRequestStatusWorkflow->canConfirm($sr)); + self::assertSame(ServiceRequestStatus::CONFIRMED, $serviceRequestStatusWorkflow->confirm($sr)->getStatus()); + } + + public function testConfirmTransitionException(): void + { + self::bootKernel(); + $serviceRequestStatusWorkflow = $this->getServiceRequestStatusWorkflow(); + $sr = $this->getServiceRequest(); + + $sr->setStatus(ServiceRequestStatus::FINISHED); + self::assertFalse($serviceRequestStatusWorkflow->canConfirm($sr)); + $this->expectException(\LogicException::class); + $serviceRequestStatusWorkflow->confirm($sr); + } + + public function testRefuseTransitionSuccess(): void + { + self::bootKernel(); + $serviceRequestStatusWorkflow = $this->getServiceRequestStatusWorkflow(); + $sr = $this->getServiceRequest() + ->setStatus(ServiceRequestStatus::NEW); + + self::assertTrue($serviceRequestStatusWorkflow->canRefuse($sr)); + self::assertSame(ServiceRequestStatus::REFUSED, $serviceRequestStatusWorkflow->refuse($sr)->getStatus()); + } + + public function testRefuseTransitionException(): void + { + self::bootKernel(); + $serviceRequestStatusWorkflow = $this->getServiceRequestStatusWorkflow(); + $sr = $this->getServiceRequest(); + + $sr->setStatus(ServiceRequestStatus::FINISHED); + self::assertFalse($serviceRequestStatusWorkflow->canRefuse($sr)); + $this->expectException(\LogicException::class); + $serviceRequestStatusWorkflow->refuse($sr); + } + + public function testFinalizeTransitionSuccess(): void + { + self::bootKernel(); + $serviceRequestStatusWorkflow = $this->getServiceRequestStatusWorkflow(); + $sr = $this->getServiceRequest() + ->setStatus(ServiceRequestStatus::CONFIRMED) + ->setStartAt(new \DateTimeImmutable('yesterday')) + ->setEndAt(new \DateTimeImmutable('today')) + ; + + self::assertTrue($serviceRequestStatusWorkflow->canFinalize($sr)); + self::assertSame(ServiceRequestStatus::FINISHED, $serviceRequestStatusWorkflow->finalize($sr)->getStatus()); + } + + public function testFinalizeTransitionException(): void + { + self::bootKernel(); + $serviceRequestStatusWorkflow = $this->getServiceRequestStatusWorkflow(); + $sr = $this->getServiceRequest(); + + self::assertFalse($serviceRequestStatusWorkflow->canFinalize($sr)); + $this->expectException(\LogicException::class); + $serviceRequestStatusWorkflow->finalize($sr); + } + + public function testAutoFinalizeTransitionSuccess(): void + { + self::bootKernel(); + $serviceRequestStatusWorkflow = $this->getServiceRequestStatusWorkflow(); + $sr = $this->getServiceRequest() + ->setStatus(ServiceRequestStatus::CONFIRMED) + ->setStartAt(new \DateTimeImmutable('- 5 days')) + ->setEndAt(new \DateTimeImmutable('- 3 days')) + ; + + self::assertTrue($serviceRequestStatusWorkflow->canAutoFinalize($sr)); + self::assertSame(ServiceRequestStatus::FINISHED, $serviceRequestStatusWorkflow->autoFinalize($sr)->getStatus()); + } + + /** + * Ongoing service request that can't be auto-finalized. + * + * @see ServiceRequestAutoFinalizeTransitionSubscriber + */ + public function testAutoFinalizeTransitionException(): void + { + self::bootKernel(); + $serviceRequestStatusWorkflow = $this->getServiceRequestStatusWorkflow(); + $sr = $this->getServiceRequest() + ->setStatus(ServiceRequestStatus::CONFIRMED) + ->setStartAt(new \DateTimeImmutable('- 3 days')) + ->setEndAt(new \DateTimeImmutable('+ 3 days')) + ; + self::assertFalse($serviceRequestStatusWorkflow->canAutoFinalize($sr)); + $this->expectException(\LogicException::class); + $serviceRequestStatusWorkflow->autoFinalize($sr); + } +} diff --git a/tests/Mock/Geocoder/GeoProviderMock.php b/tests/Mock/Geocoder/GeoProviderMock.php new file mode 100644 index 0000000..d60a4fe --- /dev/null +++ b/tests/Mock/Geocoder/GeoProviderMock.php @@ -0,0 +1,56 @@ +lower()->trim()->containsAny('ez, 000, 000')) { + return new AddressCollection(); + } + + // one dummy result + $adminLevels = new AdminLevelCollection(); + $address = new NominatimAddress( + providedBy: 'mock', + adminLevels: $adminLevels, + locality: $text, + ); + + return new AddressCollection([ + $address, + ]); + } + + /** + * Mock geocoder calls to make to CI more reliable. + */ + public function getAddress(string $text): ?Address + { + if (u($text)->lower()->trim()->containsAny('lille')) { + return (new Address()) + ->setAddress($text) + ->setLocality($text) + ->setDisplayName($text) + ->setLatitude('50.6365654') + ->setLongitude('3.0635282'); + } + + throw new \LogicException("Mock for $text not implemented"); + } +} diff --git a/tests/TestReference.php b/tests/TestReference.php new file mode 100644 index 0000000..a1387d0 --- /dev/null +++ b/tests/TestReference.php @@ -0,0 +1,130 @@ +setId($this->getUuid()) + ->setSlug('group'); + + return (new GroupOffer()) + ->setId($this->getUuid()) + ->setGroup($group); + } + + private function getUser(): User + { + return (new User()) + ->setId($this->getUuid()); + } + + /** + * @return CommandBusInterface&MockObject + */ + private function getCommandBus(): MockObject + { + return $this->getMockBuilder(CommandBusInterface::class) + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * @return GroupOfferRepository&MockObject + */ + private function getGroupOfferRepo(): GroupOfferRepository + { + $groupOfferRepo = $this->getMockBuilder(GroupOfferRepository::class) + ->disableOriginalConstructor() + ->getMock(); + $groupOfferRepo->method('find') + ->willReturn($this->getGroupOffer()); + + return $groupOfferRepo; + } + + /** + * Too complicated, the controller should be refactored. + */ + public function testUnprocessableEntityHttpException(): void + { + $httpRequestVerifierInterface = $this->getMockBuilder(HttpRequestVerifierInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $httpRequestVerifierInterface->method('verify') + ->willThrowException(new UnprocessableEntityHttpException()); + + $payum = $this->getMockBuilder(Payum::class) + ->disableOriginalConstructor() + ->getMock(); + $payum->method('getHttpRequestVerifier') + ->willReturn($httpRequestVerifierInterface); + + $translator = $this->getMockBuilder(TranslatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $doneAction = new DoneAction( + $this->getGroupOfferRepo(), + $payum, + $translator, + $this->getCommandBus() + ); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Cannot verify Payum token'); + $doneAction->__invoke(new Request(), TestReference::UUID_404, $this->getUser()); + } + + /** + * All this to test a line :/. + */ + public function testFlashWarning(): void + { + $httpRequestVerifierInterface = $this->getMockBuilder(HttpRequestVerifierInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $httpRequestVerifierInterface->method('verify') + ->willReturn(new PaymentToken()); + + $payum = $this->getMockBuilder(Payum::class) + ->disableOriginalConstructor() + ->getMock(); + $payum->method('getHttpRequestVerifier') + ->willReturn($httpRequestVerifierInterface); + + $translator = $this->getMockBuilder(TranslatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $comandBus = $this->getCommandBus(); + $comandBus->method('dispatch')->willReturn(new GetHumanStatus(new PaymentToken())); + + $doneAction = new DoneAction( + $this->getGroupOfferRepo(), + $payum, + $translator, + $comandBus + ); + + // set session! + $session = $this->getMockBuilder(FlashBagAwareSessionInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $session->method('getFlashBag')->willReturn(new FlashBag()); + + $requesStack = $this->getMockBuilder(RequestStack::class) + ->disableOriginalConstructor() + ->getMock(); + $requesStack->method('getSession')->willReturn($session); + $container = $this->getMockBuilder(ContainerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $container->method('get')->willReturn($requesStack); + $doneAction->setContainer($container); + + $this->expectException(\Error::class); // or more mock are needed. To cleanup later + $doneAction->__invoke(new Request(), TestReference::UUID_404, $this->getUser()); + } +} diff --git a/tests/Unit/Doctrine/Manager/ProductManagerTest.php b/tests/Unit/Doctrine/Manager/ProductManagerTest.php new file mode 100644 index 0000000..8475acf --- /dev/null +++ b/tests/Unit/Doctrine/Manager/ProductManagerTest.php @@ -0,0 +1,58 @@ +getMockBuilder(ProductRepository::class) + ->disableOriginalConstructor() + ->getMock(); + $translatorMock = $this->getMockBuilder(TranslatorInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $fileUploaderMock = $this->getMockBuilder(FileUploader::class) + ->disableOriginalConstructor() + ->getMock(); + $loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + // test that the logger was called buut no error 500 should be raised + $loggerMock->expects(self::once()) + ->method('warning'); + + $productStorageMock = $this->getMockBuilder(FilesystemOperator::class) + ->disableOriginalConstructor() + ->getMock(); + $exception = new UnableToDeleteFile('foo'); + $productStorageMock->method('delete')->willThrowException($exception); + $productManager = new ProductManager( + $productRepositoryMock, + $translatorMock, + $fileUploaderMock, + $productStorageMock, + $loggerMock + ); + $product = (new Product()) + ->setId(Uuid::v6()); + $productManager->deleteImage($product, 'foobar.png'); + } +} diff --git a/tests/Unit/Doctrine/Manager/UserManagerTest.php b/tests/Unit/Doctrine/Manager/UserManagerTest.php new file mode 100644 index 0000000..5a58b85 --- /dev/null +++ b/tests/Unit/Doctrine/Manager/UserManagerTest.php @@ -0,0 +1,50 @@ +getMockBuilder(FilesystemOperator::class)->disableOriginalConstructor()->getMock(); + $userStorageMock->method('delete')->willThrowException($exception); + + $loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + // test that the logger was called buut no error 500 should be raised + $loggerMock->expects(self::once()) + ->method('warning'); + + $userManager = new UserManager( + $this->getMockBuilder(UserPasswordHasherInterface::class)->disableOriginalConstructor()->getMock(), + $this->getMockBuilder(UserRepository::class)->disableOriginalConstructor()->getMock(), + $this->getMockBuilder(ClockInterface::class)->disableOriginalConstructor()->getMock(), + $this->getMockBuilder(StringHelper::class)->disableOriginalConstructor()->getMock(), + $userStorageMock, + $this->getMockBuilder(FileUploader::class)->disableOriginalConstructor()->getMock(), + $loggerMock + ); + $user = (new User()) + ->setId(Uuid::v6()) + ->setAvatar('foobar.png') + ; + $userManager->deleteAvatar($user); + } +} diff --git a/tests/Unit/Dto/Admin/AbstractFormCommandTest.php b/tests/Unit/Dto/Admin/AbstractFormCommandTest.php new file mode 100644 index 0000000..83cc9d8 --- /dev/null +++ b/tests/Unit/Dto/Admin/AbstractFormCommandTest.php @@ -0,0 +1,17 @@ +expectException(\UnexpectedValueException::class); + $command->toJsonArray(); + } +} diff --git a/tests/Unit/Dto/Admin/DummyFormCommand.php b/tests/Unit/Dto/Admin/DummyFormCommand.php new file mode 100644 index 0000000..4e34dce --- /dev/null +++ b/tests/Unit/Dto/Admin/DummyFormCommand.php @@ -0,0 +1,22 @@ + + */ + protected function getSections(): array + { + return [ + 'dummySection', + ]; + } +} diff --git a/tests/Unit/Dto/Product/SearchTest.php b/tests/Unit/Dto/Product/SearchTest.php new file mode 100644 index 0000000..608011d --- /dev/null +++ b/tests/Unit/Dto/Product/SearchTest.php @@ -0,0 +1,34 @@ +hasQuery()); + $searchDto->q = 'foobar'; + self::assertTrue($searchDto->hasQuery()); + + self::assertFalse($searchDto->hasProximity()); + self::assertFalse($searchDto->hasDistance()); + + $searchDto->distance = 5; + self::assertTrue($searchDto->hasDistance()); + self::assertFalse($searchDto->hasProximity()); + + $address = new Address(); + self::assertFalse($address->hasLocality()); + $address->setLocality('Lille'); + self::assertTrue($address->hasLocality()); + $searchDto->city = $address; + self::assertTrue($searchDto->hasProximity()); + } +} diff --git a/tests/Unit/Dto/Security/AccountCreateStep1CommandTest.php b/tests/Unit/Dto/Security/AccountCreateStep1CommandTest.php new file mode 100644 index 0000000..fa1193a --- /dev/null +++ b/tests/Unit/Dto/Security/AccountCreateStep1CommandTest.php @@ -0,0 +1,41 @@ +setEmail('foo@example.com'); + $command = new AccountCreateStep1Command($user); + self::assertSame('foo@example.com', $command->email); + } + + /** + * @return iterable + */ + public function provideConstructException(): iterable + { + yield ['']; + yield ['toto']; + } + + /** + * @dataProvider provideConstructException + */ + public function testConstructInvalidEmailException(string $email): void + { + $user = new User(); + $user->setEmail($email); + $this->expectException(InvalidArgumentException::class); + new AccountCreateStep1Command($user); + } +} diff --git a/tests/Unit/Entity/AddressTest.php b/tests/Unit/Entity/AddressTest.php new file mode 100644 index 0000000..ffc1612 --- /dev/null +++ b/tests/Unit/Entity/AddressTest.php @@ -0,0 +1,32 @@ +setId($id)->getId()); + self::assertSame('full addr', $address->setDisplayName('full addr')->getDisplayName()); + self::assertSame('full addr', (string) $address); + self::assertSame('1 bis', $address->setStreetNumber('1 bis')->getStreetNumber()); + self::assertSame('Winston Churchill', $address->setStreetName('Winston Churchill')->getStreetName()); + self::assertSame('Lille', $address->setLocality('Lille')->getLocality()); + self::assertSame('Fives', $address->setSubLocality('Fives')->getSubLocality()); + self::assertSame('Lille (Fives)', $address->getSubAndLocality()); + self::assertSame('50.63', $address->setLatitude('50.63')->getLatitude()); + self::assertSame('3.01', $address->setLongitude('3.01')->getLongitude()); + self::assertSame('nominatim', $address->setProvidedBy('nominatim')->getProvidedBy()); + self::assertSame('copyright', $address->setAttribution('copyright')->getAttribution()); + self::assertSame('way', $address->setOsmType('way')->getOsmType()); + self::assertSame(12345, $address->setOsmId(12345)->getOsmId()); + } +} diff --git a/tests/Unit/Entity/CategoryTest.php b/tests/Unit/Entity/CategoryTest.php new file mode 100644 index 0000000..7b25225 --- /dev/null +++ b/tests/Unit/Entity/CategoryTest.php @@ -0,0 +1,21 @@ +setId($id)->getId()); + self::assertSame('cat-2', $category->setSlug('cat-2')->getSlug()); + self::assertFalse($category->hasParent()); + } +} diff --git a/tests/Unit/Entity/ConfigurationTest.php b/tests/Unit/Entity/ConfigurationTest.php new file mode 100644 index 0000000..6b2340e --- /dev/null +++ b/tests/Unit/Entity/ConfigurationTest.php @@ -0,0 +1,19 @@ +getId()); + self::assertSame(ConfigurationType::INSTANCE, $configuration->setType(ConfigurationType::INSTANCE)->getType()); + } +} diff --git a/tests/Unit/Entity/DummyImage.php b/tests/Unit/Entity/DummyImage.php new file mode 100644 index 0000000..a6404c9 --- /dev/null +++ b/tests/Unit/Entity/DummyImage.php @@ -0,0 +1,15 @@ +setId($id)->getId()); + self::assertSame('EUR', $groupOffer->setCurrency('EUR')->getCurrency()); + } +} diff --git a/tests/Unit/Entity/GroupTest.php b/tests/Unit/Entity/GroupTest.php new file mode 100644 index 0000000..d3fcbf5 --- /dev/null +++ b/tests/Unit/Entity/GroupTest.php @@ -0,0 +1,48 @@ +setId($id)->getId()); + self::assertSame('grp1', $group->setName('grp1')->getName()); + self::assertSame('desc1', $group->setDescription('desc1')->getDescription()); + self::assertSame('https://example.com', $group->setUrl('https://example.com')->getUrl()); + self::assertSame(GroupType::PRIVATE, $group->setType(GroupType::PRIVATE)->getType()); + self::assertSame('grp-1', $group->setSlug('grp-1')->getSlug()); + self::assertSame(['id' => (string) $id, 'slug' => 'grp-1'], $group->getRoutingParameters()); + + $userGroup = (new UserGroup()); + self::assertCount(0, $group->getUserGroups()); + $group->addUserGroup($userGroup); + self::assertCount(1, $group->getUserGroups()); + $group->removeUserGroup($userGroup); + self::assertCount(0, $group->getUserGroups()); + + $groupOffer = new GroupOffer(); + $group->setOffers(new ArrayCollection([$groupOffer])); + self::assertTrue($group->getOffers()->contains($groupOffer)); + + $product = new Product(); + self::assertCount(0, $group->getProducts()); + $group->addProduct($product); + self::assertCount(1, $group->getProducts()); + $group->removeProduct($product); + self::assertCount(0, $group->getProducts()); + } +} diff --git a/tests/Unit/Entity/MenuItemTest.php b/tests/Unit/Entity/MenuItemTest.php new file mode 100644 index 0000000..fd4e616 --- /dev/null +++ b/tests/Unit/Entity/MenuItemTest.php @@ -0,0 +1,30 @@ +setId($id)->getId()); + self::assertSame($menu, $menuItem->setMenu($menu)->getMenu()); + self::assertSame('first item', $menuItem->setName('first item')->getName()); + self::assertSame('link', $menuItem->setLink('link')->getLink()); + self::assertSame($menuItem2, $menuItem->setParent($menuItem2)->getParent()); + self::assertSame($children, $menuItem->setChildren($children)->getChildren()); + } +} diff --git a/tests/Unit/Entity/MenuTest.php b/tests/Unit/Entity/MenuTest.php new file mode 100644 index 0000000..70ff1e8 --- /dev/null +++ b/tests/Unit/Entity/MenuTest.php @@ -0,0 +1,32 @@ +setId($id)->getId()); + self::assertSame('logo', $menu->setLogo('logo')->getImage()); + self::assertSame('menu', $menu->setCode('menu')->getCode()); + self::assertSame($items, $menu->setItems($items)->getItems()); + + self::assertSame(0, $menu->getItems()->count()); + $menuItem = new MenuItem(); + $menu->addItem($menuItem); + self::assertSame(1, $menu->getItems()->count()); + $menu->removeItem($menuItem); + self::assertSame(0, $menu->getItems()->count()); + } +} diff --git a/tests/Unit/Entity/MessageTest.php b/tests/Unit/Entity/MessageTest.php new file mode 100644 index 0000000..9b2429b --- /dev/null +++ b/tests/Unit/Entity/MessageTest.php @@ -0,0 +1,77 @@ +setId($id)->getId()); + self::assertSame(MessageType::SYSTEM, $message->setType(MessageType::SYSTEM)->getType()); + self::assertSame($serviceRequest, $message->setServiceRequest($serviceRequest)->getServiceRequest()); + self::assertSame('foobar', $message->setMessage('foobar')->getMessage()); + self::assertSame('foobar', (string) $message); + self::assertNull($message->getMessageTemplate()); + self::assertEmpty($message->getMessageParameters()); + + self::assertSame(MessageType::FROM_OWNER, $message->setType(MessageType::FROM_OWNER)->getType()); + self::assertTrue($message->getType()->isFromOwner()); + self::assertFalse($message->getType()->isFromRecipient()); + + self::assertSame(MessageType::FROM_RECIPIENT, $message->setType(MessageType::FROM_RECIPIENT)->getType()); + self::assertFalse($message->getType()->isFromOwner()); + self::assertTrue($message->getType()->isFromRecipient()); + + self::assertFalse($message->isOwnerRead()); + self::assertNull($message->getOwnerReadAt()); + + self::assertFalse($message->isRecipientRead()); + self::assertNull($message->getRecipientReadAt()); + } + + public function testMessageGetRecipient(): void + { + $message = new Message(); + $serviceRequest = new ServiceRequest(); + $message->setServiceRequest($serviceRequest); + $owner = new User(); + $recipent = new User(); + $serviceRequest->setOwner($owner)->setRecipient($recipent); + + $message->setType(MessageType::FROM_OWNER); + self::assertSame($owner, $message->getSender()); + self::assertSame($recipent, $message->getRecipient()); + + $message->setType(MessageType::FROM_RECIPIENT); + self::assertSame($recipent, $message->getSender()); + self::assertSame($owner, $message->getRecipient()); + } + + public function testMessageGetSenderException(): void + { + $message = new Message(); + self::assertSame(MessageType::SYSTEM, $message->setType(MessageType::SYSTEM)->getType()); + $this->expectException(\LogicException::class); + $message->getSender(); + } + + public function testMessageGetRecipientException(): void + { + $message = new Message(); + self::assertSame(MessageType::SYSTEM, $message->setType(MessageType::SYSTEM)->getType()); + $this->expectException(\LogicException::class); + $message->getRecipient(); + } +} diff --git a/tests/Unit/Entity/PageTest.php b/tests/Unit/Entity/PageTest.php new file mode 100644 index 0000000..305adf2 --- /dev/null +++ b/tests/Unit/Entity/PageTest.php @@ -0,0 +1,20 @@ +setId($id)->getId()); + self::assertSame('page', $category->setSlug('page')->getSlug()); + } +} diff --git a/tests/Unit/Entity/PaymentTest.php b/tests/Unit/Entity/PaymentTest.php new file mode 100644 index 0000000..0c0a87d --- /dev/null +++ b/tests/Unit/Entity/PaymentTest.php @@ -0,0 +1,51 @@ +setId((string) $id)->getId()); + $user = new User(); + self::assertSame($user, $payment->setUser($user)->getUser()); + } + + public function testPaymentIsPaid(): void + { + $payment = new Payment(); + self::assertFalse($payment->isPaid()); + self::assertNull($payment->getStatus()); + + // offline + $payment->setDetails([ + 'paid' => true, + 'status' => 'captured', + ]); + self::assertTrue($payment->isPaid()); + self::assertSame('captured', $payment->getStatus()); + + // test and prod mode + $payment->setDetails([ + 'paymment' => [], + ]); + self::assertFalse($payment->isPaid()); + self::assertNull($payment->getStatus()); + $payment->setDetails([ + 'payment' => [ + 'status' => 'paid', + ], + ]); + self::assertTrue($payment->isPaid()); + self::assertSame('paid', $payment->getStatus()); + } +} diff --git a/tests/Unit/Entity/ProductAvailibilityTest.php b/tests/Unit/Entity/ProductAvailibilityTest.php new file mode 100644 index 0000000..373fea5 --- /dev/null +++ b/tests/Unit/Entity/ProductAvailibilityTest.php @@ -0,0 +1,35 @@ +setId($id)->getId()); + + $product = new Product(); + self::assertSame($product, $productAvailability->setProduct($product)->getProduct()); + + $serviceRequest = new ServiceRequest(); + self::assertSame($serviceRequest, $productAvailability->setServiceRequest($serviceRequest)->getServiceRequest()); + + self::assertSame(ProductAvailabilityType::OWNER, $productAvailability->setType(ProductAvailabilityType::OWNER)->getType()); + + $product->setName('foobar'); + $today = date_create_immutable('2023-02-09'); + $productAvailability->setStartAt($today)->setEndAt($today); + self::assertSame('foobar / 2023-02-09 / 2023-02-09', (string) $productAvailability); + } +} diff --git a/tests/Unit/Entity/ProductTest.php b/tests/Unit/Entity/ProductTest.php new file mode 100644 index 0000000..9afef7e --- /dev/null +++ b/tests/Unit/Entity/ProductTest.php @@ -0,0 +1,131 @@ +setCategory($category); + $id = Uuid::v6(); + self::assertSame($id, $product->setId($id)->getId()); + self::assertSame('prd1', $product->setSlug('prd1')->getSlug()); + self::assertSame('age', $product->setAge('age')->getAge()); + self::assertNull($product->getFirstImage()); + self::assertNull($product->getSubCategory()); + self::assertSame($category, $product->getMainCategory()); + self::assertSame(['img1.png'], $product->setImages(['img1.png'])->getImages()); + self::assertSame('2 jours', $product->setPreferredLoanDuration('2 jours')->getPreferredLoanDuration()); + $product->setPaused(); + self::assertTrue($product->setActive()->getStatus()->isActive()); + + self::assertFalse($product->hasServiceRequests()); + $serviceRequestCollection = new ArrayCollection(); + $sr = new ServiceRequest(); + $serviceRequestCollection->add($sr); + $product->setServiceRequests($serviceRequestCollection); + self::assertSame($serviceRequestCollection, $product->getServiceRequests()); + self::assertTrue($product->hasServiceRequests()); + + $availabilities = new ArrayCollection(); + $productAvailability = new ProductAvailability(); + $availabilities->add($productAvailability); + $product->setAvailabilities($availabilities); + self::assertCount(1, $product->getAvailabilities()); + + self::assertCount(0, $product->getGroups()); + $group = new Group(); + $product->addGroup($group); + self::assertCount(1, $product->getGroups()); + $product->removeGroup($group); + self::assertCount(0, $product->getGroups()); + } + + public function testProductObject(): void + { + $product = new Product(); + self::assertSame(100, $product->setDeposit(100)->getDeposit()); + self::assertSame('EUR', $product->setCurrency('EUR')->getCurrency()); + } + + public function testGetUnavailabilities(): void + { + $product = new Product(); + self::assertEmpty($product->getUnavailabilities(null)); + + $today = new \DateTimeImmutable('today'); + $tomorrow = new \DateTimeImmutable('tomorrow'); + $pa = (new ProductAvailability()) + ->setProduct($product) + ->setType(ProductAvailabilityType::OWNER) + ->setMode(ProductAvailabilityMode::UNAVAILABLE) + ->setStartAt($today) + ->setEndAt($tomorrow) + ; + $product->addAvailability($pa); + + self::assertCount(1, $product->getAvailabilities()); + self::assertCount(2, $product->getUnavailabilities()); // 2 days + self::assertSame([ + $today->format('Y-m-d'), + $tomorrow->format('Y-m-d'), + ], $product->getUnavailabilities(null)); + + // now assign the sr to the product unavailability + $sr = (new ServiceRequest())->setProduct($product); + $pa->setServiceRequest($sr); + + self::assertCount(2, $product->getUnavailabilities()); + // days are exclude now because the service request is not taken in account + self::assertCount(0, $product->getUnavailabilities($sr)); + + $product->removeAvailability($pa); + self::assertCount(0, $product->getAvailabilities()); + } + + /** + * Non regression test for issue #395 in historic repo. + */ + public function testBug395(): void + { + $product = new Product(); + $product->setImages([ + 'image1', + 'image2', + 'image3', + ]); + + $product->deleteImage('image2'); + self::assertSame(['image1', 'image3'], $product->getImages()); + } + + /** + * Non regression test for #463 in historic repo. + */ + public function testSetImages(): void + { + $product = new Product(); + $product->setImages([ + '', + null, + 'image3', + ]); + + self::assertSame(['image3'], $product->getImages()); + } +} diff --git a/tests/Unit/Entity/ServiceRequestTest.php b/tests/Unit/Entity/ServiceRequestTest.php new file mode 100644 index 0000000..8e360e6 --- /dev/null +++ b/tests/Unit/Entity/ServiceRequestTest.php @@ -0,0 +1,84 @@ +setId($id)->getId()); + + $message = new Message(); + $id = Uuid::v6(); + self::assertSame($id, $message->setId($id)->getId()); + + $product = new Product(); + $product->setType(ProductType::OBJECT); + $sr->setProduct($product); + self::assertTrue($sr->isLoan()); + + $product->setType(ProductType::SERVICE); + self::assertTrue($sr->isService()); + + $sr->setRecipient(new User()); + self::assertFalse($sr->isRecipient(new User())); + + $sr->setOwner(new User()); + self::assertFalse($sr->isOwner(new User())); + + self::assertCount(0, $sr->getMessages()); + self::assertSame(0, $sr->messagesCount()); + + $sr->addMessage($message); + self::assertCount(1, $sr->getMessages()); + self::assertSame(1, $sr->messagesCount()); + + $sr->removeMessage($message); + self::assertCount(0, $sr->getMessages()); + self::assertSame(0, $sr->messagesCount()); + + $collection = new ArrayCollection(); + $collection->add($message); + + $sr->setMessages($collection); + self::assertCount(1, $sr->getMessages()); + self::assertSame(1, $sr->messagesCount()); + } + + /** + * In the functional tests. The message collection is empty and therefore the + * hasUnreadMessages function is not tested correctly. + */ + public function testServiceRequestHasUnreadMessages(): void + { + $sr = new ServiceRequest(); + $owner = new User(); + $sr->setOwner($owner); + + $message = new Message(); + $message->setOwnerRead(false); + $sr->addMessage($message); + self::assertTrue($sr->hasUnreadMessages($owner)); + $message->setOwnerRead(true); + self::assertFalse($sr->hasUnreadMessages($owner)); + + $recipient = new User(); + $sr->setRecipient($recipient); + self::assertTrue($sr->hasUnreadMessages($recipient)); + $message->setRecipientRead(true); + self::assertFalse($sr->hasUnreadMessages($recipient)); + } +} diff --git a/tests/Unit/Entity/UserGroupTest.php b/tests/Unit/Entity/UserGroupTest.php new file mode 100644 index 0000000..88f4034 --- /dev/null +++ b/tests/Unit/Entity/UserGroupTest.php @@ -0,0 +1,46 @@ +setId($id)->getId()); + $user = new User(); + self::assertSame($user, $userGroup->setUser($user)->getUser()); + $group = new Group(); + $date = new \DateTimeImmutable('now'); + self::assertSame($group, $userGroup->setGroup($group)->getGroup()); + self::assertSame(UserMembership::ADMIN, $userGroup->setMembership(UserMembership::ADMIN)->getMembership()); + + // todelete when payment tests are done + $payedAt = new \DateTimeImmutable('now'); + self::assertSame($payedAt, $userGroup->setPayedAt($payedAt)->getPayedAt()); + } + + public function testSetMember(): void + { + $userGroup = new UserGroup(); + self::assertSame(UserMembership::INVITATION, $userGroup->getMembership()); + + $userGroup->setMember(); + self::assertSame(UserMembership::MEMBER, $userGroup->getMembership()); + + $userGroup->setMembership(UserMembership::ADMIN); + $userGroup->setMember(); + // must stay admin + self::assertTrue($userGroup->getMembership()->isAdmin()); + } +} diff --git a/tests/Unit/Entity/UserTest.php b/tests/Unit/Entity/UserTest.php new file mode 100644 index 0000000..16b5d2f --- /dev/null +++ b/tests/Unit/Entity/UserTest.php @@ -0,0 +1,63 @@ +setId($id)->getId()); + self::assertNull($user->getType()); + self::assertTrue($user->setMainAdminAccount(true)->isMainAdminAccount()); + self::assertTrue($user->setDevAccount(true)->isDevAccount()); + $user->eraseCredentials(); + + $user->setType(UserType::USER); + self::assertSame('firstname', $user->setFirstname('firstname')->getDisplayName()); + $user->setFirstname('x'); + + $user->setType(UserType::PLACE); + self::assertSame('name', $user->setName('name')->getDisplayName()); + $user->setName('x'); + + $user->setType(UserType::ADMIN); + self::assertSame('admin', $user->setFirstname('admin')->getDisplayName()); + + $group = new Group(); + $userGroup = (new UserGroup()) + ->setGroup($group); + + self::assertCount(0, $user->getUserGroups()); + $user->addUserGroup($userGroup); + self::assertCount(1, $user->getRoles()); + self::assertSame(['ROLE_USER'], $user->getRoles()); + self::assertCount(1, $user->getUserGroups()); + self::assertSame([$group], $user->getMyGroups()->toArray()); + self::assertSame([$group], $user->getMyGroups()->toArray()); // with local cache + + $user->removeUserGroup($userGroup); + self::assertCount(0, $user->getUserGroups()); + + self::assertCount(0, $user->getPayments()); + $payment = new Payment(); + $user->setPayments(new ArrayCollection([$payment])); + self::assertCount(1, $user->getPayments()); + + // test exception case for getPhone. + $user->setPhoneNumber('foobar'); + self::assertNull($user->getPhone()); + } +} diff --git a/tests/Unit/Enum/Group/GroupMembershipTest.php b/tests/Unit/Enum/Group/GroupMembershipTest.php new file mode 100644 index 0000000..61d82cc --- /dev/null +++ b/tests/Unit/Enum/Group/GroupMembershipTest.php @@ -0,0 +1,17 @@ +isCharged()); + self::assertTrue(GroupMembership::FREE->isFree()); + } +} diff --git a/tests/Unit/Enum/Group/GroupOfferTypeTest.php b/tests/Unit/Enum/Group/GroupOfferTypeTest.php new file mode 100644 index 0000000..392f199 --- /dev/null +++ b/tests/Unit/Enum/Group/GroupOfferTypeTest.php @@ -0,0 +1,17 @@ +isMonthly()); + self::assertTrue(GroupOfferType::YEARLY->isYearly()); + } +} diff --git a/tests/Unit/Enum/Group/UserMembershipTest.php b/tests/Unit/Enum/Group/UserMembershipTest.php new file mode 100644 index 0000000..7f4c545 --- /dev/null +++ b/tests/Unit/Enum/Group/UserMembershipTest.php @@ -0,0 +1,18 @@ +isInvited()); + self::assertTrue(UserMembership::MEMBER->isMember()); + self::assertTrue(UserMembership::ADMIN->isAdmin()); + } +} diff --git a/tests/Unit/Enum/ServiceRequest/ServiceRequestStatusTest.php b/tests/Unit/Enum/ServiceRequest/ServiceRequestStatusTest.php new file mode 100644 index 0000000..c6b1e3b --- /dev/null +++ b/tests/Unit/Enum/ServiceRequest/ServiceRequestStatusTest.php @@ -0,0 +1,30 @@ +isNew()); + self::assertTrue(ServiceRequestStatus::TO_CONFIRM->isToConfirm()); + self::assertTrue(ServiceRequestStatus::CONFIRMED->isConfirmed()); + self::assertTrue(ServiceRequestStatus::REFUSED->isRefused()); + self::assertTrue(ServiceRequestStatus::FINISHED->isFinished()); + } + + public function testUserTypeIsOngoing(): void + { + self::assertTrue(ServiceRequestStatus::NEW->isOngoing()); + self::assertTrue(ServiceRequestStatus::TO_CONFIRM->isOngoing()); + self::assertTrue(ServiceRequestStatus::CONFIRMED->isOngoing()); + + self::assertFalse(ServiceRequestStatus::REFUSED->isOngoing()); + self::assertFalse(ServiceRequestStatus::FINISHED->isOngoing()); + } +} diff --git a/tests/Unit/Enum/User/UserTypeTest.php b/tests/Unit/Enum/User/UserTypeTest.php new file mode 100644 index 0000000..5189305 --- /dev/null +++ b/tests/Unit/Enum/User/UserTypeTest.php @@ -0,0 +1,21 @@ + 'admin', + 'USER' => 'user', + 'PLACE' => 'place', + ], $typesAsArray); + } +} diff --git a/tests/Unit/Geocoder/GeoProviderTest.php b/tests/Unit/Geocoder/GeoProviderTest.php new file mode 100644 index 0000000..32cf410 --- /dev/null +++ b/tests/Unit/Geocoder/GeoProviderTest.php @@ -0,0 +1,70 @@ +getMockBuilder(Provider::class) + ->disableOriginalConstructor() + ->getMock(); + $nominatimGeocoder->method('geocodeQuery') + ->willThrowException(new InvalidServerResponse()); + + $geoProvider = new GeoProvider($nominatimGeocoder, new NominatimToAddressAdapter()); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unable to get geoloc'); + $geoProvider->getAddressCollection('foo', 1); + } + + public function testEmptyCollectionSuccess(): void + { + $nominatimGeocoder = $this->getMockBuilder(Provider::class) + ->disableOriginalConstructor() + ->getMock(); + $nominatimGeocoder->method('geocodeQuery') + ->willReturn(new AddressCollection()); + + $geoProvider = new GeoProvider($nominatimGeocoder, new NominatimToAddressAdapter()); + self::assertNull($geoProvider->getAddress('foo')); + } + + public function testGetAddressSuccess(): void + { + $nominatimAddress = new NominatimAddress( + providedBy: 'mock', + adminLevels: new AdminLevelCollection(), + coordinates: new Coordinates(50.63, 3.01), + locality: 'Lille', + ); + $nominatimAddress = $nominatimAddress + ->withOSMId(6576374058) + ->withOSMType('node'); + $addressCollection = new AddressCollection([$nominatimAddress]); + $nominatimGeocoder = $this->getMockBuilder(Provider::class) + ->disableOriginalConstructor() + ->getMock(); + $nominatimGeocoder->method('geocodeQuery') + ->willReturn($addressCollection); + + $geoProvider = new GeoProvider($nominatimGeocoder, new NominatimToAddressAdapter()); + self::assertNotNull($geoProvider->getAddress('Lille')); + } +} diff --git a/tests/Unit/Helper/StringHelperTest.php b/tests/Unit/Helper/StringHelperTest.php new file mode 100644 index 0000000..ef552a8 --- /dev/null +++ b/tests/Unit/Helper/StringHelperTest.php @@ -0,0 +1,19 @@ +humanize('UUID')); // not converted + self::assertSame('Login At', $stringHelper->humanize('loginAt')); + self::assertSame('Firstname', $stringHelper->humanize('firstname')); + } +} diff --git a/tests/Unit/Helper/VardumperHelperTest.php b/tests/Unit/Helper/VardumperHelperTest.php new file mode 100644 index 0000000..5f0a673 --- /dev/null +++ b/tests/Unit/Helper/VardumperHelperTest.php @@ -0,0 +1,18 @@ +getMockBuilder(TexterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $texterMock->method('send') + ->willThrowException(new TransportException('invalid number', new MockResponse('foobar'))); + + $loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $notifier = new SmsNotifier($texterMock, $loggerMock); + + $user = new User(); + $user->setSmsNotifications(true) + ->setPhoneNumber('+33610101010'); + $sentMessage = $notifier->notify($user, 'subject'); + self::assertNull($sentMessage); + } +} diff --git a/tests/Unit/Security/Voter/ProductVoterTest.php b/tests/Unit/Security/Voter/ProductVoterTest.php new file mode 100644 index 0000000..3909d29 --- /dev/null +++ b/tests/Unit/Security/Voter/ProductVoterTest.php @@ -0,0 +1,44 @@ +setOwner($user); + } + + public function testVoteRequestServiceVoterUserNotLoggedAccessDenied(): void + { + $voter = new ProductVoter(); + $subject = $this->createProduct(); + $user = $subject->getOwner(); + $token = new UsernamePasswordToken($user, 'main', $user->getRoles()); + + self::assertSame(VoterInterface::ACCESS_DENIED, $voter->vote($token, $subject, [ProductVoter::BORROW])); + self::assertSame(VoterInterface::ACCESS_GRANTED, $voter->vote($token, $subject, [ProductVoter::EDIT])); + self::assertSame(VoterInterface::ACCESS_GRANTED, $voter->vote($token, $subject, [ProductVoter::DUPLICATE])); + + $otherUser = new User(); + $token = new UsernamePasswordToken($otherUser, 'main', $otherUser->getRoles()); + self::assertSame(VoterInterface::ACCESS_GRANTED, $voter->vote($token, $subject, [ProductVoter::BORROW])); + self::assertSame(VoterInterface::ACCESS_DENIED, $voter->vote($token, $subject, [ProductVoter::EDIT])); + self::assertSame(VoterInterface::ACCESS_DENIED, $voter->vote($token, $subject, [ProductVoter::DUPLICATE])); + } +} diff --git a/tests/Unit/Security/Voter/ServiceRequest/ServiceRequestVoterTest.php b/tests/Unit/Security/Voter/ServiceRequest/ServiceRequestVoterTest.php new file mode 100644 index 0000000..b0e85ca --- /dev/null +++ b/tests/Unit/Security/Voter/ServiceRequest/ServiceRequestVoterTest.php @@ -0,0 +1,38 @@ +setOwner($owner) + ->setRecipient($recipient); + } + + public function testVoteRequestServiceVoterUserNotLoggedAccessDenied(): void + { + $voter = new ServiceRequestVoter(); + $token = new NullToken(); // act as if the user was not logged + $subject = $this->createServiceRequest(); + $attribute = ServiceRequestVoter::VIEW; + + self::assertSame(VoterInterface::ACCESS_DENIED, $voter->vote($token, $subject, [$attribute])); + } +} diff --git a/tests/Unit/Subscriber/SecuritySubscriberTest.php b/tests/Unit/Subscriber/SecuritySubscriberTest.php new file mode 100644 index 0000000..7f873bf --- /dev/null +++ b/tests/Unit/Subscriber/SecuritySubscriberTest.php @@ -0,0 +1,17 @@ + 'onLoginSuccess'], SecuritySubscriber::getSubscribedEvents()); + } +} diff --git a/tests/Unit/Validator/Constraints/Category/CategoryParentNotSelfValidatorTest.php b/tests/Unit/Validator/Constraints/Category/CategoryParentNotSelfValidatorTest.php new file mode 100644 index 0000000..ed12885 --- /dev/null +++ b/tests/Unit/Validator/Constraints/Category/CategoryParentNotSelfValidatorTest.php @@ -0,0 +1,54 @@ + + */ +final class CategoryParentNotSelfValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): CategoryParentNotSelfValidator + { + return new CategoryParentNotSelfValidator(); + } + + public function testIsValid(): void + { + $this->validator->validate(new Category(), new CategoryParentNotSelf()); + $this->assertNoViolation(); + } + + public function testIsInvalid(): void + { + $group = new Category(); + $group->setParent($group); + + $this->validator->validate($group, new CategoryParentNotSelf()); + $this->buildViolation('validator.category.parentnotself') + ->atPath('property.path.parent') + ->assertRaised(); + } + + public function testInvalidValueType(): void + { + $this->expectException(UnexpectedValueException::class); + $this->validator->validate(new User(), new CategoryParentNotSelf()); + } + + public function testInvalidConstraintType(): void + { + $this->expectException(UnexpectedTypeException::class); + $this->validator->validate('foo', new Length(['max' => 5])); + } +} diff --git a/tests/Unit/Validator/Constraints/FileValidatorTest.php b/tests/Unit/Validator/Constraints/FileValidatorTest.php new file mode 100644 index 0000000..d78f550 --- /dev/null +++ b/tests/Unit/Validator/Constraints/FileValidatorTest.php @@ -0,0 +1,38 @@ + + */ +final class FileValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): FileValidator + { + return new FileValidator(); + } + + public function testDontvalidateString(): void + { + $this->validator->validate('foo', new File()); + $this->assertNoViolation(); + } + + public function testWithUploadedFile(): void + { + $image = realpath(__DIR__.'/../../../Fixtures/images/apes.png'); + $uploaddedDile = new UploadedFile((string) $image, 'apes.png'); + $this->validator->validate($uploaddedDile, new File()); + $this->buildViolation('The file could not be uploaded.') + ->atPath('property.path') + ->setCode('0') + ->assertRaised(); + } +} diff --git a/tests/Unit/Validator/Constraints/Group/GroupParentNotSelfValidatorTest.php b/tests/Unit/Validator/Constraints/Group/GroupParentNotSelfValidatorTest.php new file mode 100644 index 0000000..d7e3b7d --- /dev/null +++ b/tests/Unit/Validator/Constraints/Group/GroupParentNotSelfValidatorTest.php @@ -0,0 +1,54 @@ + + */ +final class GroupParentNotSelfValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): GroupParentNotSelfValidator + { + return new GroupParentNotSelfValidator(); + } + + public function testIsValid(): void + { + $this->validator->validate(new Group(), new GroupParentNotSelf()); + $this->assertNoViolation(); + } + + public function testIsInvalid(): void + { + $group = new Group(); + $group->setParent($group); + + $this->validator->validate($group, new GroupParentNotSelf()); + $this->buildViolation('validator.group.groupparentnotself') + ->atPath('property.path.parent') + ->assertRaised(); + } + + public function testInvalidValueType(): void + { + $this->expectException(UnexpectedValueException::class); + $this->validator->validate(new User(), new GroupParentNotSelf()); + } + + public function testInvalidConstraintType(): void + { + $this->expectException(UnexpectedTypeException::class); + $this->validator->validate('foo', new Length(['max' => 5])); + } +} diff --git a/tests/Unit/Validator/Constraints/MenuItem/MenuItemParentNotSelfValidatorTest.php b/tests/Unit/Validator/Constraints/MenuItem/MenuItemParentNotSelfValidatorTest.php new file mode 100644 index 0000000..e9100bb --- /dev/null +++ b/tests/Unit/Validator/Constraints/MenuItem/MenuItemParentNotSelfValidatorTest.php @@ -0,0 +1,54 @@ + + */ +final class MenuItemParentNotSelfValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): MenuItemParentNotSelfValidator + { + return new MenuItemParentNotSelfValidator(); + } + + public function testIsValid(): void + { + $this->validator->validate(new MenuItem(), new MenuItemParentNotSelf()); + $this->assertNoViolation(); + } + + public function testIsInvalid(): void + { + $MenuItem = new MenuItem(); + $MenuItem->setParent($MenuItem); + + $this->validator->validate($MenuItem, new MenuItemParentNotSelf()); + $this->buildViolation('validator.menuitem.parentnotself') + ->atPath('property.path.parent') + ->assertRaised(); + } + + public function testInvalidValueType(): void + { + $this->expectException(UnexpectedValueException::class); + $this->validator->validate(new User(), new MenuItemParentNotSelf()); + } + + public function testInvalidConstraintType(): void + { + $this->expectException(UnexpectedTypeException::class); + $this->validator->validate('foo', new Length(['max' => 5])); + } +} diff --git a/tests/Unit/Validator/Constraints/ServiceRequest/ProductAvailabilityNoOverlapValidatorTest.php b/tests/Unit/Validator/Constraints/ServiceRequest/ProductAvailabilityNoOverlapValidatorTest.php new file mode 100644 index 0000000..b51cfab --- /dev/null +++ b/tests/Unit/Validator/Constraints/ServiceRequest/ProductAvailabilityNoOverlapValidatorTest.php @@ -0,0 +1,49 @@ + + */ +final class ProductAvailabilityNoOverlapValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): ProductAvailabilityNoOverlapValidator + { + return new ProductAvailabilityNoOverlapValidator(); + } + + public function testIsValid(): void + { + $sr = (new ServiceRequest()) + ->setProduct(new Product()) + ->setStartAt(new \DateTimeImmutable('today')) + ->setEndAt(new \DateTimeImmutable('tomorrow')) + ; + $this->validator->validate($sr, new ProductAvailabilityNoOverlap()); + $this->assertNoViolation(); + } + + public function testInvalidValueType(): void + { + $this->expectException(UnexpectedValueException::class); + $this->validator->validate(new User(), new ProductAvailabilityNoOverlap()); + } + + public function testInvalidConstraintType(): void + { + $this->expectException(UnexpectedTypeException::class); + $this->validator->validate('foo', new Length(['max' => 5])); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..ab081fd --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,13 @@ +bootEnv(dirname(__DIR__).'/.env'); +// } diff --git a/translations/.gitignore b/translations/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/translations/EasyAdminBundle.fr.xlf b/translations/EasyAdminBundle.fr.xlf new file mode 100644 index 0000000..d736540 --- /dev/null +++ b/translations/EasyAdminBundle.fr.xlf @@ -0,0 +1,16 @@ + + + +
+ +
+ + + + files + Fichiers + + + +
+
diff --git a/translations/admin.en.xlf b/translations/admin.en.xlf new file mode 100644 index 0000000..0e24f4b --- /dev/null +++ b/translations/admin.en.xlf @@ -0,0 +1,15 @@ + + + +
+ +
+ + + + dashboard.title + Plateforme EBS + + +
+
diff --git a/translations/admin.fr.xlf b/translations/admin.fr.xlf new file mode 100644 index 0000000..ef10811 --- /dev/null +++ b/translations/admin.fr.xlf @@ -0,0 +1,358 @@ + + + +
+ +
+ + + + dashboard.title + Plateforme EBS + + + + dashboard.statistics + Statistiques + + + + + menu.dashboard + Dashboard + + + + menu.administrators + Administrateur·rice·s + + + + menu.parameters + Paramètres + + + + menu.menu + Menu + + + + menu.pages + Pages + + + + menu.config_menu + Menu + + + + menu.config_footer + Pied de page + + + + menu.menu + Menu + + + + menu.content + Contenus + + + + menu.categories + Catégories + + + + menu.usage + Utilisation + + + + menu.users + Utilisateur·rice + + + + menu.places + Lieux + + + + menu.groups + Groupes + + + + menu.members + Membres + + + + menu.articles + Produits + + + + menu.objects + Objets + + + + menu.services + Services + + + + menu.loans + Emprunts + + + + menu.public + Site public + + + + menu.home + Page d'accueil + + + + menu.user + Espace utilisateur·rice + + + + menu.devtools + DEV Tools + + + + menu.poc_payoum + POC Payoum + + + + menu.poc_meilisearch + POC Meilisearch + + + + menu.dev_tools + Outils + + + + menu.poc_email_notification + Test notif. email + + + + menu.poc_sms_notification + Test notif. SMS + + + + menu.poc_geoloc + Test de géolocalisation + + + + + id + Identifiant + + + + Created At + Créé le + + + + Updated At + Mis à jour le + + + + Type + Type + + + + Enabled + Activé ? + + + + Email Confirmed + Email Confirmé ? + + + + crud.entitylabelinsingular + + + + + + groups + Groupes + + + + Group + Groupe + + + + Is Public + Public ? + + + + Name + Nom + + + + Description + Description + + + + URL + URL + + + + Type + Type + + + + PUBLIC + public + + + + PRIVATE + privé + + + + Parent + Parent + + + + Link + Lien + + + + + parameters.content_title + Paramètres de l'instance + + + + parameters.senders.h2 + Expéditeur·rice des notifications + + + + parameters.contac.h2t + Contact + + + + parameters.groups.h2 + Groupes + + + + parameters.products.h2 + Paramètres des produits + + + + parameters.confidentiality.h2 + Confidentialité + + + + parameters_controller.form.success + Configuration sauvegardée + + + + + yes + oui + + + + no + non + + + + + panel.tech_informations + Informations techniques + + + + panel.information + Informations + + + + + images.help + Formats autorisés : %extensions%. Taille maximum %upload_maxsize_by_file% mégaoctet. + + + + 1 Fichiers + 1 fichier + + + + 2 Fichiers + 2 fichiers + + + + action.save + Sauvegarder + + + + date.the + le + + + + date.on + à + + + + user.by + par + + + + action.back + Retour + + + +
+
diff --git a/translations/app/Controller/Admin/AbstractCategoryCrudController/admin.fr.xlf b/translations/app/Controller/Admin/AbstractCategoryCrudController/admin.fr.xlf new file mode 100644 index 0000000..3ffd5d6 --- /dev/null +++ b/translations/app/Controller/Admin/AbstractCategoryCrudController/admin.fr.xlf @@ -0,0 +1,25 @@ + + + +
+ +
+ + + app.controller.admin.abstract_category_crud_controller.menu.action.move_up + Monter + + + + app.controller.admin.abstract_category_crud_controller.menu.action.move_down + Descendre + + + + app.controller.admin.abstract_category_crud_controller.move.success + Catégorie modifiée. + + + +
+
diff --git a/translations/app/Controller/Admin/AbstractUserCrudController/admin.fr.xlf b/translations/app/Controller/Admin/AbstractUserCrudController/admin.fr.xlf new file mode 100644 index 0000000..77694f2 --- /dev/null +++ b/translations/app/Controller/Admin/AbstractUserCrudController/admin.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ +
+ + + app.controller.admin.abstract_user_crud_controller.field.phone.help + Numéro de téléphone au format international, ex : +33610101010 + + + +
+
diff --git a/translations/app/Controller/Admin/GroupOfferCrudController/admin.fr.xlf b/translations/app/Controller/Admin/GroupOfferCrudController/admin.fr.xlf new file mode 100644 index 0000000..7c41541 --- /dev/null +++ b/translations/app/Controller/Admin/GroupOfferCrudController/admin.fr.xlf @@ -0,0 +1,44 @@ + + + +
+ +
+ + + group_offers + Tarifs d'adhésion + + + + GroupOffer + Tarif d'adhésion + + + + ONESHOT + unique + + + + YEARLY + annuel + + + + MONTHLY + mensuel + + + + Active + Actif ? + + + + Price + prix + + +
+
diff --git a/translations/app/Controller/Admin/PaymentCrudController/admin.fr.xlf b/translations/app/Controller/Admin/PaymentCrudController/admin.fr.xlf new file mode 100644 index 0000000..f4a848b --- /dev/null +++ b/translations/app/Controller/Admin/PaymentCrudController/admin.fr.xlf @@ -0,0 +1,65 @@ + + + +
+ +
+ + + ID + ID + + + + payments + Paiements + + + + Payments + Paiements + + + + Payment + Paiement + + + + Method + Méthode de prélèvement + + + + Total Amount + Montant total + + + + Paid + Payé ? + + + + Client Email + Email associé au compte au moment du paiement + + + + Number + Identifiant Payum + + + + Client Id + ID compte client + + + + Details + Détails + + + +
+
diff --git a/translations/app/Controller/Admin/UserGroupCrudController/admin.fr.xlf b/translations/app/Controller/Admin/UserGroupCrudController/admin.fr.xlf new file mode 100644 index 0000000..769734b --- /dev/null +++ b/translations/app/Controller/Admin/UserGroupCrudController/admin.fr.xlf @@ -0,0 +1,50 @@ + + + +
+ +
+ + + UserGroup + adhésion + + + + user_groups + Membres + + + + MEMBER + Membre + + + + INVITATION + Invitation + + + + List Members + Voir les membres + + + + app.controller.admin.group_crud_controller.invite.flash.success + Invitation envoyée. + + + + app.controller.admin.user_group_crud_controller.expires_in.formatted_value + %days% jour(s). + + + + Expires In + Expire dans ? + + + +
+
diff --git a/translations/app/Controller/Group/GroupController/messages.fr.xlf b/translations/app/Controller/Group/GroupController/messages.fr.xlf new file mode 100644 index 0000000..01c288b --- /dev/null +++ b/translations/app/Controller/Group/GroupController/messages.fr.xlf @@ -0,0 +1,19 @@ + + + +
+ +
+ + + app.controller.group.group_controller.mail_subject + Demande de création de groupe + + + + app.controller.group.group_controller.mail_success + Votre demande de création de groupe a bien été envoyée + + +
+
diff --git a/translations/app/Controller/Payment/DoneAction/messages.fr.xlf b/translations/app/Controller/Payment/DoneAction/messages.fr.xlf new file mode 100644 index 0000000..c0b0d1e --- /dev/null +++ b/translations/app/Controller/Payment/DoneAction/messages.fr.xlf @@ -0,0 +1,34 @@ + + + +
+ +
+ + + app.controller.payment.done_action.flash.success + Paiement réussi. Vous êtes désormais membre du groupe %group%. + + + + app.controller.payment.done_action.status.new + Le paiement est en cours mais n'a pas encore abouti. + + + + app.controller.payment.done_action.status.failed + Le paiement a échoué, vous n'avez pas été prélevé·e. + + + + app.controller.payment.done_action.status.canceled + Le paiement a été annulé, vous n'avez pas été prélevé·e. + + + + app.controller.payment.done_action.status.expired + Le paiement a expiré, veuillez réessayer. + + +
+
diff --git a/translations/app/Controller/Security/AccountCreateController/messages.fr.xlf b/translations/app/Controller/Security/AccountCreateController/messages.fr.xlf new file mode 100644 index 0000000..eac3165 --- /dev/null +++ b/translations/app/Controller/Security/AccountCreateController/messages.fr.xlf @@ -0,0 +1,34 @@ + + + +
+ +
+ + + app.controller.security.account_create_controller.step1.flash.success + Vous allez recevoir un lien par e-mail pour valider votre adresse e-mail. Cliquez sur ce lien pour poursuivre la création de votre compte. + + + + app.controller.security.account_create_controller.step2.flash.success + Votre compte a bien été créé. Veuillez renseigner votre adresse afin de pouvoir utiliser les différents services. + + + + app.controller.security.account_create_controller.step2.with_invitation.flash.success + Votre compte a bien été créé. Vous pouvez accepter l'invitation sur la page du groupe. + + + + app.controller.security.account_create_controller.step2.user_not_found.warning + Aucun·e utilisateur·rice correspondant à ce code n'a été trouvé·e. Si votre compte est déjà confirmé veuillez vous connecter. + + + + app.controller.security.account_create_controller.step2.user_confirmation_token_expired.warning + Votre jeton de sécurité a expiré. Un nouvel email de confirmation de compte vient de vous être envoyé. + + +
+
diff --git a/translations/app/Controller/User/Account/ChangeLoginAction/messages.fr.xlf b/translations/app/Controller/User/Account/ChangeLoginAction/messages.fr.xlf new file mode 100644 index 0000000..3f33d84 --- /dev/null +++ b/translations/app/Controller/User/Account/ChangeLoginAction/messages.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + app.controller.user.account.change_login_action.flash.success + Votre email a bien été modifié. + + +
+
diff --git a/translations/app/Controller/User/Account/ChangePasswordAction/messages.fr.xlf b/translations/app/Controller/User/Account/ChangePasswordAction/messages.fr.xlf new file mode 100644 index 0000000..1e13b83 --- /dev/null +++ b/translations/app/Controller/User/Account/ChangePasswordAction/messages.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + app.controller.user.account.change_password_action.flash.success + Votre mot de passe a bien été modifié. + + +
+
diff --git a/translations/app/Controller/User/Account/DeleteUserAvatarAction/messages.fr.xlf b/translations/app/Controller/User/Account/DeleteUserAvatarAction/messages.fr.xlf new file mode 100644 index 0000000..15b2c0a --- /dev/null +++ b/translations/app/Controller/User/Account/DeleteUserAvatarAction/messages.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + app.controller.user.account.delete_user_avatar_action.flash.success + La photo a bien été supprimée. + + +
+
diff --git a/translations/app/Controller/User/Account/EditProfileAction/messages.fr.xlf b/translations/app/Controller/User/Account/EditProfileAction/messages.fr.xlf new file mode 100644 index 0000000..96aed69 --- /dev/null +++ b/translations/app/Controller/User/Account/EditProfileAction/messages.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + app.controller.user.account.edit_profile_action.flash.success + Votre profil a bien été mis à jour. + + +
+
diff --git a/translations/app/Controller/User/Group/UserGroupController/messages.fr.xlf b/translations/app/Controller/User/Group/UserGroupController/messages.fr.xlf new file mode 100644 index 0000000..9c364cc --- /dev/null +++ b/translations/app/Controller/User/Group/UserGroupController/messages.fr.xlf @@ -0,0 +1,24 @@ + + + +
+ +
+ + + app.controller.user.group.user_group_controller.flash.success + Vous avez bien rejoint le groupe. + + + + app.controller.user.group.user_group_controller.accept_invitation.flash.success + L'invitation a bien été acceptée. + + + + app.controller.user.group.user_group_controller.quit_group.flash.success + Vous avez bien quitté·e le groupe. + + +
+
diff --git a/translations/app/Controller/User/Product/DeleteProductAction/messages.fr.xlf b/translations/app/Controller/User/Product/DeleteProductAction/messages.fr.xlf new file mode 100644 index 0000000..ae0fc76 --- /dev/null +++ b/translations/app/Controller/User/Product/DeleteProductAction/messages.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + app.controller.user.product.delete_product_action.flash.success + La produit a bien été supprimé. + + +
+
diff --git a/translations/app/Controller/User/Product/DeleteProductAvailabilityAction/messages.fr.xlf b/translations/app/Controller/User/Product/DeleteProductAvailabilityAction/messages.fr.xlf new file mode 100644 index 0000000..5aec942 --- /dev/null +++ b/translations/app/Controller/User/Product/DeleteProductAvailabilityAction/messages.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + app.controller.user.product.delete_product_unavailability_action.flash.success + L'indisponibilité du produit à bien été supprimée. + + +
+
diff --git a/translations/app/Controller/User/Product/DeleteProductPhotoAction/messages.fr.xlf b/translations/app/Controller/User/Product/DeleteProductPhotoAction/messages.fr.xlf new file mode 100644 index 0000000..f3dfc5b --- /dev/null +++ b/translations/app/Controller/User/Product/DeleteProductPhotoAction/messages.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ + prefix : app.controller.user.product.delete_product_photo_action +
+ + + app.controller.user.product.delete_product_photo_action.flash.success + La photo a bien été supprimée. + + +
+
diff --git a/translations/app/Controller/User/Product/DuplicateProductAction/messages.fr.xlf b/translations/app/Controller/User/Product/DuplicateProductAction/messages.fr.xlf new file mode 100644 index 0000000..343c664 --- /dev/null +++ b/translations/app/Controller/User/Product/DuplicateProductAction/messages.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + app.controller.user.product.duplicate_product_action.flash.success + Le produit a bien été dupliqué. + + +
+
diff --git a/translations/app/Controller/User/ServiceRequest/ConversationController/messages.fr.xlf b/translations/app/Controller/User/ServiceRequest/ConversationController/messages.fr.xlf new file mode 100644 index 0000000..d36fc62 --- /dev/null +++ b/translations/app/Controller/User/ServiceRequest/ConversationController/messages.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + app.controller.user.service_request.conversation_controller.flash.success + Le message a bien été envoyé. + + +
+
diff --git a/translations/app/Controller/User/ServiceRequest/ServiceRequestWorkflowController/messages.fr.xlf b/translations/app/Controller/User/ServiceRequest/ServiceRequestWorkflowController/messages.fr.xlf new file mode 100644 index 0000000..a26c7f1 --- /dev/null +++ b/translations/app/Controller/User/ServiceRequest/ServiceRequestWorkflowController/messages.fr.xlf @@ -0,0 +1,69 @@ + + + +
+ +
+ + + app.controller.user.service_request.service_request_status_workflow_controller.flash.object.accept + La demande de prêt a bien été acceptée, l'emprunteur·euse vient d'être notifié·e par email. + + + + app.controller.user.service_request.service_request_status_workflow_controller.flash.service.accept + La demande de service a bien été acceptée, le·la bénéficiaire vient d'être notifié·e par email. + + + + app.controller.user.service_request.service_request_status_workflow_controller.flash.object.modify_owner + La demande d'emprunt a bien été modifiée, l'emprunteur·euse vient d'être notifié·e par email. + + + + app.controller.user.service_request.service_request_status_workflow_controller.flash.service.modify_owner + La demande de service a bien été modifiée, le·la bénéficiaire vient d'être notifié·e par email. + + + + app.controller.user.service_request.service_request_status_workflow_controller.flash.object.modify_recipient + La demande d'emprunt a bien été modifiée, le·la propriétaire vient d'être notifié·e par email. + + + + app.controller.user.service_request.service_request_status_workflow_controller.flash.service.modify_recipient + La demande de service a bien été modifiée, le·la propriétaire vient d'être notifié·e par email. + + + + app.controller.user.service_request.service_request_status_workflow_controller.flash.object.confirm + La demande de prêt a bien été confirmée, l'emprunteur·euse vient d'être notifié·e par email. + + + + app.controller.user.service_request.service_request_status_workflow_controller.flash.service.confirm + La demande de service a bien été confirmée, le·la bénéficiaire vient d'être notifié·e par email. + + + + app.controller.user.service_request.service_request_status_workflow_controller.flash.object.refuse + La demande de prêt a bien été refusée / annulée. + + + + app.controller.user.service_request.service_request_status_workflow_controller.flash.service.refuse + La demande de service a bien été refusée / annulée. + + + + app.controller.user.service_request.service_request_status_workflow_controller.flash.object.finalize + La demande de prêt a bien été finalisée. + + + + app.controller.user.service_request.service_request_status_workflow_controller.flash.service.finalize + La demande de service a bien été finalisée. + + +
+
diff --git a/translations/app/Controller/User/VacationModeAction/messages.fr.xlf b/translations/app/Controller/User/VacationModeAction/messages.fr.xlf new file mode 100644 index 0000000..cdec498 --- /dev/null +++ b/translations/app/Controller/User/VacationModeAction/messages.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + app.controller.user.vacation_mode_action.flash.success + Le mode vacances a bien été mis à jour. + + +
+
diff --git a/translations/app/Doctrine/Manager/MessageManager/messages_system.fr.xlf b/translations/app/Doctrine/Manager/MessageManager/messages_system.fr.xlf new file mode 100644 index 0000000..28c2214 --- /dev/null +++ b/translations/app/Doctrine/Manager/MessageManager/messages_system.fr.xlf @@ -0,0 +1,69 @@ + + + +
+ +
+ + + app.doctrine.manager.message_manager.message.system.new + Début de la demande d'emprunt - Par %recipient% - Du %startAt% au %endAt%. + + + + app.doctrine.manager.message_manager.message.system.accepted.object + La demande a été acceptée par le·la propriétaire de l'objet. + + + + app.doctrine.manager.message_manager.message.system.accepted.service + La demande a été acceptée par le·la prestataire du service. + + + + app.doctrine.manager.message_manager.message.system.modified_by_owner.dobject + Les dates de prêt ont été modifiées par le·la propriétaire de l'objet, du %startAt% au %endAt%. + + + + app.doctrine.manager.message_manager.message.system.modified_by_owner.service + Les dates de la demande ont été modifiées par le·la propriétaire service, du %startAt% au %endAt%. + + + + app.doctrine.manager.message_manager.message.system.modified_by_recipient.dobject + Les dates de prêt ont été modifiées par l'emprunteur·euse, du %startAt% au %endAt%. + + + + app.doctrine.manager.message_manager.message.system.modified_by_recipient.service + Les dates de la demande ont été modifiées par le·la bénéficiaire du service, du %startAt% au %endAt%. + + + + app.doctrine.manager.message_manager.message.system.confirmed.object + La demande a été confirmée par l'emprunteur·euse. + + + + app.doctrine.manager.message_manager.message.system.confirmed.service + La demande a été confirmée par le·la bénéficiaire du service. + + + + app.doctrine.manager.message_manager.message.system.refused.object + La demande d'emprunt a été refusée / annulée par %actor%. + + + + app.doctrine.manager.message_manager.message.system.refused.service + La demande de service a été refusée / annulée par %actor%. + + + + app.doctrine.manager.message_manager.message.system.finalized + La demande de service a été finalisée. + + +
+
diff --git a/translations/app/Doctrine/Manager/ProductManager/messages.fr.xlf b/translations/app/Doctrine/Manager/ProductManager/messages.fr.xlf new file mode 100644 index 0000000..67fab45 --- /dev/null +++ b/translations/app/Doctrine/Manager/ProductManager/messages.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ + prefix: app.doctrine.manager.product_manager +
+ + + app.doctrine.manager.product_manager.duplicate.copy_of + Copie de + + +
+
diff --git a/translations/app/Doctrine/Manager/ServiceRequestManager/messages.fr.xlf b/translations/app/Doctrine/Manager/ServiceRequestManager/messages.fr.xlf new file mode 100644 index 0000000..adf1b2e --- /dev/null +++ b/translations/app/Doctrine/Manager/ServiceRequestManager/messages.fr.xlf @@ -0,0 +1,19 @@ + + + +
+ +
+ + + src.doctrine.manager.service_request_manager.message.object.default + Bonjour. Je voudrais emprunter votre %product%, svp. 🙂 + + + + src.doctrine.manager.service_request_manager.message.object.default + Bonjour. Je suis intéressé·e par votre service "%product%". 🙂 + + +
+
diff --git a/translations/app/Entity/Product/validators.fr.xlf b/translations/app/Entity/Product/validators.fr.xlf new file mode 100644 index 0000000..e5f1b97 --- /dev/null +++ b/translations/app/Entity/Product/validators.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ +
+ + + app.entity.product.groups.constraints.count.min_message + Vous devez selectionner au moins un groupe. + + + +
+
diff --git a/translations/app/Enum/Group/GroupType/messages.fr.xlf b/translations/app/Enum/Group/GroupType/messages.fr.xlf new file mode 100644 index 0000000..0f4a3f7 --- /dev/null +++ b/translations/app/Enum/Group/GroupType/messages.fr.xlf @@ -0,0 +1,20 @@ + + + +
+ +
+ + + app.enum.group.group_type.public + Public + + + + app.enum.group.group_type.private + Privé + + + +
+
diff --git a/translations/app/Form/Type/Product/ObjectFormType/messages.fr.xlf b/translations/app/Form/Type/Product/ObjectFormType/messages.fr.xlf new file mode 100644 index 0000000..41e3751 --- /dev/null +++ b/translations/app/Form/Type/Product/ObjectFormType/messages.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ +
+ + + app.form.type.product.object_form_type.form.groups + Visible dans mes groupes : + + + +
+
diff --git a/translations/app/Form/Type/Product/SearchFormType/messages.fr.xlf b/translations/app/Form/Type/Product/SearchFormType/messages.fr.xlf new file mode 100644 index 0000000..81173be --- /dev/null +++ b/translations/app/Form/Type/Product/SearchFormType/messages.fr.xlf @@ -0,0 +1,50 @@ + + + +
+ +
+ + + app.form.type.product.search_form_type.q.placeholder + Mots-clés + + + + app.form.type.product.search_form_type.category.placeholder + Catégorie + + + + app.form.type.product.search_form_type.place.placeholder + Lieu partenaire + + + + app.form.type.product.search_form_type.object + Objets + + + + app.form.type.product.search_form_type.service + Services + + + + app.form.type.product.search_form_type.proximity.label + Distance + + + + app.form.type.product.search_form_type.submit.label + Rechercher + + + + app.form.type.product.search_form_type.distance.placeholder + Pas de limite de distance + + + +
+
diff --git a/translations/app/Form/Type/Product/ServiceFormType/messages.fr.xlf b/translations/app/Form/Type/Product/ServiceFormType/messages.fr.xlf new file mode 100644 index 0000000..9c405a3 --- /dev/null +++ b/translations/app/Form/Type/Product/ServiceFormType/messages.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ +
+ + + app.form.type.product.service_form_type.form.groups + Visible dans mes groupes : + + + +
+
diff --git a/translations/app/Form/Type/Security/GroupInvitationFormType/admin.fr.xlf b/translations/app/Form/Type/Security/GroupInvitationFormType/admin.fr.xlf new file mode 100644 index 0000000..c538d0f --- /dev/null +++ b/translations/app/Form/Type/Security/GroupInvitationFormType/admin.fr.xlf @@ -0,0 +1,21 @@ + + + +
+ +
+ + + + app.form.type.security.group_invitation_form_type.email + Email : + + + + app.form.type.security.group_invitation_form_type.submit + Envoyer l'invitation + + + +
+
diff --git a/translations/app/Form/Type/User/AddressStep2FormType/messages.fr.xlf b/translations/app/Form/Type/User/AddressStep2FormType/messages.fr.xlf new file mode 100644 index 0000000..06fb8a3 --- /dev/null +++ b/translations/app/Form/Type/User/AddressStep2FormType/messages.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ +
+ + + app.form.type.user.address_step2_form_type.submit.label + Enregistrer + + + +
+
diff --git a/translations/app/Form/Type/User/AddressStep2FormType/validators.fr.xlf b/translations/app/Form/Type/User/AddressStep2FormType/validators.fr.xlf new file mode 100644 index 0000000..9005326 --- /dev/null +++ b/translations/app/Form/Type/User/AddressStep2FormType/validators.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ +
+ + + app.form.type.user.address_step2_form_type.addresses.not_null + Veuillez choisir une adresse. + + + +
+
diff --git a/translations/app/Mailer/Email/Admin/Group/GroupInvitationEmail/email.fr.xlf b/translations/app/Mailer/Email/Admin/Group/GroupInvitationEmail/email.fr.xlf new file mode 100644 index 0000000..f84c7a2 --- /dev/null +++ b/translations/app/Mailer/Email/Admin/Group/GroupInvitationEmail/email.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + app.mailer.email.admin.group.group_invitation_email.subject + %brand% : invitation dans le groupe %group%. + + +
+
diff --git a/translations/app/Mailer/Email/Admin/UserGroup/AdminPromotion/email.fr.xlf b/translations/app/Mailer/Email/Admin/UserGroup/AdminPromotion/email.fr.xlf new file mode 100644 index 0000000..e50eebd --- /dev/null +++ b/translations/app/Mailer/Email/Admin/UserGroup/AdminPromotion/email.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + app.mailer.email.admin.user_group.admin_promotion_email.subject + %brand% : changement de rôle + + +
+
diff --git a/translations/app/Mailer/Email/Admin/UserGroup/MainAdminPromotion/email.fr.xlf b/translations/app/Mailer/Email/Admin/UserGroup/MainAdminPromotion/email.fr.xlf new file mode 100644 index 0000000..b84d8df --- /dev/null +++ b/translations/app/Mailer/Email/Admin/UserGroup/MainAdminPromotion/email.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + app.mailer.email.admin.user_group.main_admin_promotion_email.subject + %brand% : changement de rôle + + +
+
diff --git a/translations/app/Mailer/Email/Command/EndMembershipEmail/email.fr.xlf b/translations/app/Mailer/Email/Command/EndMembershipEmail/email.fr.xlf new file mode 100644 index 0000000..d0437cf --- /dev/null +++ b/translations/app/Mailer/Email/Command/EndMembershipEmail/email.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ +
+ + + app.mailer.email.command.end_membership_email.subject + %brand% : adhésion au groupe %group% terminée. + + + +
+
diff --git a/translations/app/Mailer/Email/Command/NotifyMembershipExpiration/email.fr.xlf b/translations/app/Mailer/Email/Command/NotifyMembershipExpiration/email.fr.xlf new file mode 100644 index 0000000..309cce2 --- /dev/null +++ b/translations/app/Mailer/Email/Command/NotifyMembershipExpiration/email.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ +
+ + + app.mailer.email.command.notify_membership_expiration_email.subject + %brand% : expiration de l'adhésion au groupe %group% dans %days% jour(s). + + + +
+
diff --git a/translations/app/Mailer/Email/Command/NotifyServiceRequestEndEmail/email.fr.xlf b/translations/app/Mailer/Email/Command/NotifyServiceRequestEndEmail/email.fr.xlf new file mode 100644 index 0000000..a728845 --- /dev/null +++ b/translations/app/Mailer/Email/Command/NotifyServiceRequestEndEmail/email.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ +
+ + + app.mailer.email.command.notify_service_request_end_email.subject + %brand% : Votre emprunt concernant "%product%" se termine demain %date%. + + + +
+
diff --git a/translations/app/Mailer/Email/Command/NotifyServiceRequestStartEmail/email.fr.xlf b/translations/app/Mailer/Email/Command/NotifyServiceRequestStartEmail/email.fr.xlf new file mode 100644 index 0000000..d877fb2 --- /dev/null +++ b/translations/app/Mailer/Email/Command/NotifyServiceRequestStartEmail/email.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ +
+ + + app.mailer.email.command.notify_service_request_start_email.subject + %brand% : Votre emprunt concernant "%product%" débute demain %date%. + + + +
+
diff --git a/translations/app/Mailer/Email/Security/CreateAccountStep1Email/email.fr.xlf b/translations/app/Mailer/Email/Security/CreateAccountStep1Email/email.fr.xlf new file mode 100644 index 0000000..b2f3d3a --- /dev/null +++ b/translations/app/Mailer/Email/Security/CreateAccountStep1Email/email.fr.xlf @@ -0,0 +1,19 @@ + + + +
+ +
+ + + app.mailer.email.security.create_account_step1_email.subject + Confirmation de votre compte %brand% : échanges de bien et services + + + + app.mailer.email.security.create_account_step1_email.invitation.subject.invitation + %brand% : invitation dans le groupe %group%. + + +
+
diff --git a/translations/app/Mailer/Email/ServiceRequest/NewServiceRequest/email.fr.xlf b/translations/app/Mailer/Email/ServiceRequest/NewServiceRequest/email.fr.xlf new file mode 100644 index 0000000..a91b492 --- /dev/null +++ b/translations/app/Mailer/Email/ServiceRequest/NewServiceRequest/email.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ +
+ + + app.mailer.email.service_request.new_service_request.subject + %brand% : nouvelle demande d'emprunt + + + +
+
diff --git a/translations/app/Mailer/Email/ServiceRequest/ServiceRequestAccepted/email.fr.xlf b/translations/app/Mailer/Email/ServiceRequest/ServiceRequestAccepted/email.fr.xlf new file mode 100644 index 0000000..46c0cbd --- /dev/null +++ b/translations/app/Mailer/Email/ServiceRequest/ServiceRequestAccepted/email.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + app.mailer.email.service_request.service_request_accepted.subject + %brand% : emprunt accepté + + +
+
diff --git a/translations/app/Mailer/Email/ServiceRequest/ServiceRequestConfirmed/email.fr.xlf b/translations/app/Mailer/Email/ServiceRequest/ServiceRequestConfirmed/email.fr.xlf new file mode 100644 index 0000000..72ece35 --- /dev/null +++ b/translations/app/Mailer/Email/ServiceRequest/ServiceRequestConfirmed/email.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + app.mailer.email.service_request.service_request_confirmed.subject + %brand% : emprunt confirmé + + +
+
diff --git a/translations/app/Mailer/Email/ServiceRequest/ServiceRequestModifiedOwner/email.fr.xlf b/translations/app/Mailer/Email/ServiceRequest/ServiceRequestModifiedOwner/email.fr.xlf new file mode 100644 index 0000000..22362d4 --- /dev/null +++ b/translations/app/Mailer/Email/ServiceRequest/ServiceRequestModifiedOwner/email.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + app.mailer.email.service_request.service_request_modified_by_owner.subject + %brand% : emprunt modifié. + + +
+
diff --git a/translations/app/Mailer/Email/ServiceRequest/ServiceRequestModifiedRecipient/email.fr.xlf b/translations/app/Mailer/Email/ServiceRequest/ServiceRequestModifiedRecipient/email.fr.xlf new file mode 100644 index 0000000..094b0c1 --- /dev/null +++ b/translations/app/Mailer/Email/ServiceRequest/ServiceRequestModifiedRecipient/email.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + app.mailer.email.service_request.service_request_modified_by_recipient.subject + %brand% : emprunt modifié. + + +
+
diff --git a/translations/app/Mailer/Email/ServiceRequest/ServiceRequestRefused/email.fr.xlf b/translations/app/Mailer/Email/ServiceRequest/ServiceRequestRefused/email.fr.xlf new file mode 100644 index 0000000..3e4a80b --- /dev/null +++ b/translations/app/Mailer/Email/ServiceRequest/ServiceRequestRefused/email.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + app.mailer.email.service_request.service_request_refused.subject + %brand% : emprunt annulé. + + +
+
diff --git a/translations/category/admin.fr.xlf b/translations/category/admin.fr.xlf new file mode 100644 index 0000000..df212d0 --- /dev/null +++ b/translations/category/admin.fr.xlf @@ -0,0 +1,46 @@ + + + +
+ +
+ + + + categories.objects + Catégories des objets + + + + categories.services + Catégories des services + + + + category + catégorie + + + + Image + Image + + + + image.default + Image par défaut + + + + image.help + Image utilisée pour les produits si ceux-ci n'ont pas d'image associée. + + + + category.parent + Catégorie parente + + + +
+
diff --git a/translations/category/validators.fr.xlf b/translations/category/validators.fr.xlf new file mode 100644 index 0000000..b9ed89e --- /dev/null +++ b/translations/category/validators.fr.xlf @@ -0,0 +1,16 @@ + + + +
+ +
+ + + + validator.category.parentnotself + Vous ne pouvez pas choisir cette catégorie comme parent. + + + +
+
diff --git a/translations/group/admin.fr.xlf b/translations/group/admin.fr.xlf new file mode 100644 index 0000000..c2b00d8 --- /dev/null +++ b/translations/group/admin.fr.xlf @@ -0,0 +1,69 @@ + + + +
+ +
+ + + Membership + Type adhésion + + + + CHARGED + payante + + + + FREE + gratuite + + + + Invitation By Admin + Envoi d'invitations par les admins possible + + + + Invitation By Moderator + Envoi d'invitations par les modérateur·rice·s possible + + + + Invitation By Member + Envoi d'invitations par les membres possible + + + + Children + Enfant(s) + + + + User Groups + Membres + + + + Main Admin Account + Gérant·e du groupe + + + + Payed At + Payé le + + + + invite + Inviter + + + + offers_list + Voir les tarifs d'adhésion + + +
+
diff --git a/translations/group/messages.fr.xlf b/translations/group/messages.fr.xlf new file mode 100644 index 0000000..3da52b6 --- /dev/null +++ b/translations/group/messages.fr.xlf @@ -0,0 +1,24 @@ + + + +
+ +
+ + + PRIVATE + Privé + + + + CHARGED + Payant + + + + FREE + Gratuit + + +
+
diff --git a/translations/group/validators.fr.xlf b/translations/group/validators.fr.xlf new file mode 100644 index 0000000..84f71e6 --- /dev/null +++ b/translations/group/validators.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + validator.group.groupparentnotself + Vous ne pouvez pas choisir ce groupe comme parent. + + +
+
diff --git a/translations/menu/admin.fr.xlf b/translations/menu/admin.fr.xlf new file mode 100644 index 0000000..da1d1d9 --- /dev/null +++ b/translations/menu/admin.fr.xlf @@ -0,0 +1,183 @@ + + + +
+ +
+ + + + + Logo + Logo + + + + Code + Code + + + + + menu.fields + Liens du menu + + + + menu.field + Lien du menu + + + + LINK + Lien texte + + + + Link Type + Type de lien + + + + SOCIAL_NETWORK + Lien icône + + + + Media Type + Réseau social + + + + Menu + Menu + + + + FACEBOOK + Facebook + + + + TWITTER + Twitter + + + + MASTODON + Mastodon + + + + INSTAGRAM + Instagram + + + + YOUTUBE + Youtube + + + + LINKEDIN + Linkedin + + + + WEB + Web + + + + Items Count + Nombre de liens + + + + Items + Liens + + + + menu.page_title + Personnalisation du menu + + + + footer.page_title + Personnalisation du pied de page + + + + menu.action.edit_logo + Modifier le logo + + + + menu.action.edit_logo + Retour aux liens + + + + menu.action.up_item + Monter + + + + menu.action.down_item + Descendre + + + + menu.action.delete + Supprimer + + + + menu_items.title + Liens du menu + + + + footer_items.title + Liens du pied de page + + + + menu_item.entitylabelinsingular + lien + + + + menu_item.update_successful + L'ordre des liens a bien été modifié. + + + + Position + Position + + + + Position Human + Position + + + + icon.menu + Créer lien icône + + + + icon.menu + Créer lien texte + + + + + menu.contact + Contactez-nous + + +
+
diff --git a/translations/menu/validators.fr.xlf b/translations/menu/validators.fr.xlf new file mode 100644 index 0000000..097f2e2 --- /dev/null +++ b/translations/menu/validators.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + menu.validator.field + Ce champ ne doit pas être vide. + + +
+
diff --git a/translations/menuitem/validators.fr.xlf b/translations/menuitem/validators.fr.xlf new file mode 100644 index 0000000..c680dea --- /dev/null +++ b/translations/menuitem/validators.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ +
+ + + validator.menuitem.parentnotself + Vous ne pouvez pas choisir cet élément comme parent. + + + +
+
diff --git a/translations/message/email.fr.xlf b/translations/message/email.fr.xlf new file mode 100644 index 0000000..849ee23 --- /dev/null +++ b/translations/message/email.fr.xlf @@ -0,0 +1,16 @@ + + + +
+ +
+ + + + new_message.subject + Nouveau message sur votre espace %brand% + + + +
+
diff --git a/translations/message/messages.fr.xlf b/translations/message/messages.fr.xlf new file mode 100644 index 0000000..421bbab --- /dev/null +++ b/translations/message/messages.fr.xlf @@ -0,0 +1,26 @@ + + + +
+ +
+ + + + message.type.system + Message automatique + + + + message.type.from_owner + Message du propriétaire + + + + message.type.from_recipient + Message du bénéficiaire + + + +
+
diff --git a/translations/page/admin.fr.xlf b/translations/page/admin.fr.xlf new file mode 100644 index 0000000..293fad0 --- /dev/null +++ b/translations/page/admin.fr.xlf @@ -0,0 +1,45 @@ + + + +
+ +
+ + + Pages + Pages + + + + pages + pages + + + + Page + Page + + + + Slug + Slug + + + + Content + Contenu + + + + page.action.link + Accéder à la page + + + + Home + Page d'accueil ? + + + +
+
diff --git a/translations/parameters/admin.fr.xlf b/translations/parameters/admin.fr.xlf new file mode 100644 index 0000000..e232273 --- /dev/null +++ b/translations/parameters/admin.fr.xlf @@ -0,0 +1,85 @@ + + + +
+ +
+ + + + + parameter.mail + Adresse e-mail + + + + parameter.name + Nom + + + + + parameter.formVisibility + Lien contact visible + + + + parameter.receptionEmail + E-mail de réception + + + + + parameter.groups + Groupes + + + + parameter.groupsCreation + Création de groupe + + + + parameter.groupsCreationForAll + Ouvert à tou·te·s + + + + parameter.groupsCreationOnlyForAdmin + Uniquement par les administrateur·rice·s de l'instance + + + + parameter.paidGroupsCreation + Création de groupe payante + + + + + parameter.commentsOnArticles + Commentaires sur les articles + + + + parameter.ratingOnArticles + Notes sur les articles + + + + + parameter.conversationsVisibility + Conversations visibles par l'admin + + + + parameter.conversationsVisibilityWarning + Si vous activez l'accès aux conversations par les administrateur·rice·s, vous vous engagez à respecter la vie privée des utilisateur·rice·s et à n'utiliser cet accès qu'en cas de nécessité comme la résolution d'un conflit + + + + parameter.save + Enregister + + +
+
diff --git a/translations/product/admin.fr.xlf b/translations/product/admin.fr.xlf new file mode 100644 index 0000000..ba15165 --- /dev/null +++ b/translations/product/admin.fr.xlf @@ -0,0 +1,167 @@ + + + +
+ +
+ + + + + products + Produits + + + + Product + Produit + + + + OBJECT + objet + + + + SERVICE + service + + + + Owner + Propriétaire·rice + + + + Age + Âge / état + + + + Duration + Durée de la prestation + + + + Deposit + Caution + + + + Currency + Devise + + + + Status + Statut + + + + ACTIVE + actif + + + + PAUSED + en pause + + + + DELETED + supprimé + + + + Visibility + Visibilité + + + + PUBLIC + public + + + + RESTRICTED + restreinte à certains groupes + + + + Groups + Groupes + + + + action.onBreak + Mettre en pause + + + + action.activate + Rendre actif + + + + action.availability + Disponibilité + + + + Address + Adresse + + + + Preferred Loan Duration + Durée d'emprunt souhaitée + + + + Images + Images + + + + Product Link + Accéder à la fiche produit + + + + type + Type de produit + + + + + product.availability + Disponibilité + + + + object + Objet + + + + service + Service + + + + active + Actif + + + + owner + Prêteur·euse + + + + product.unavailabilities + Date(s) d'indisponibibilé + + +
+
diff --git a/translations/product/messages.fr.xlf b/translations/product/messages.fr.xlf new file mode 100644 index 0000000..78dcc31 --- /dev/null +++ b/translations/product/messages.fr.xlf @@ -0,0 +1,221 @@ + + + +
+ +
+ + + + product.info_age + Âge + + + + product.info_deposit + Caution + + + + product.info_duration + Durée de la prestation souhaitée: + + + product.info_loan_duration + Durée d'emprunt souhaitée: + + + + product.info_more + Voir plus + + + + product.list_name + Rechercher un article + + + + product.list_type_objects + Objets + + + + product.list_type_services + Services + + + + product.list_filter + Affiner la recherche par : + + + + product.list_filter_category + Catégorie + + + + product.list_filter_partner + Chez un·e partenaire + + + + product.list_filter_partner_place + Lieu partenaire + + + + product.list_filter_city + Ville + + + + product.list_filter_proximity + A proximité + + + + product.list_filter_search + Rechercher + + + + product.new.calendar_title + Dates d'emprunt + + + + product.show.calendar_title + Disponibilité + + + + new_product.form.visibility_for + Mon objet est visible pour : + + + + PUBLIC + Tou·te·s + + + + RESTRICTED + Uniquement mes groupes + + + + + new_product.available + Je mets à disposition : + + + + product.form.category + Catégorie + + + + product.form.description + Description + + + + product.form.visibility + Visibilité + + + + product.form.images + Images + + + + product.form.images_default + Si vous n'ajoutez aucune image, nous appliquerons l'image par défaut de la catégorie sélectionnée. + + + + product.form.upload_infos + Poids max: 1Mo, format: JPG ou PNG + + + + product.form.upload_multiple + Vous pouvez sélectionner plusieurs images dans la fenêtre de sélection de fichiers. + + + + product.form.recover + A récupérer + + + + product.form.address + Adresse + + + + product.form.schedules + Horaires + + + + product.form.submit + Enregistrer + + + + + + edit_service.form.title + Modifier un service + + + + service.form.name + Nom du service + + + + new_service.form.serviceDuration + Durée de la prestation + + + + + + edit.object.form.title + Modifier un objet + + + + object.form.name + Nom de l'objet + + + + object.form.age + Âge de l'objet + + + + object.form.preferredLoanDuration + Durée d’emprunt souhaitée + + + + object.form.deposit + Montant de la caution + + + + + product.show.unavailability + Ajouter une indisponibilité + + + +
+
diff --git a/translations/product/product_availability/messages.fr.xlf b/translations/product/product_availability/messages.fr.xlf new file mode 100644 index 0000000..1919e2c --- /dev/null +++ b/translations/product/product_availability/messages.fr.xlf @@ -0,0 +1,29 @@ + + + +
+ +
+ + + app.controller.user.product.product_availability_controller.success + Les disponibilités de votre produit ont bien été mises à jour. + + + + app.form.type.product.create_product_availability_type.startAt + Date de début + + + + app.form.type.product.create_product_availability_type.endAt + Date de fin + + + + app.form.type.product.create_product_availability_type.submit + Enregister + + +
+
diff --git a/translations/security.en.xlf b/translations/security.en.xlf new file mode 100644 index 0000000..6e80700 --- /dev/null +++ b/translations/security.en.xlf @@ -0,0 +1,16 @@ + + + +
+ +
+ + + + login.disabled_account_exception + Votre account isn't activated yet. + + + +
+
diff --git a/translations/security.fr.xlf b/translations/security.fr.xlf new file mode 100644 index 0000000..4818efc --- /dev/null +++ b/translations/security.fr.xlf @@ -0,0 +1,22 @@ + + + +
+ +
+ + + + + login.account_email_not_confirmed_exception + Vous devez confirmer votre email avec le lien qui vous a été envoyé avant de pouvoir vous identifier. + + + + login.disabled_account_exception + Votre compte est désactivé, veuillez prendre contact avec un·e administrateur·rice. + + + +
+
diff --git a/translations/security/account_create/security.fr.xlf b/translations/security/account_create/security.fr.xlf new file mode 100644 index 0000000..39821d5 --- /dev/null +++ b/translations/security/account_create/security.fr.xlf @@ -0,0 +1,89 @@ + + + +
+ +
+ + + account_create_action.title + Créer un compte + + + + account_create_action.already-logged + Vous êtes déjà loggé·e + + + + account_create_action.account_type + Type de compte + + + + USER + Individuel + + + + PLACE + Lieu + + + + account_create_action.firsname + Prénom + + + + account_create_action.firsname.placeholder + Jeanne + + + + account_create_action.lastname + Nom + + + + account_create_action.lastname.placeholder + Martin + + + + account_create_action.name + Nom du lieu + + + + account_create_action.name.placeholder + Mon association + + + + account_create_action.password.first + Mot de passe + + + + account_create_action.password.second + Confirmer le mot de passe + + + + account_create_action.password.help + 8 caractères minimun + + + + account_create_action.gdpr + J’ai lu et j’accepte les Conditions Générales d'Utilisation]]> + + + + account_create_action.info + Un email avec un lien de validation vous sera envoyé. Vous avez 24 heures pour confirmer votre compte. + + +
+
diff --git a/translations/security/account_create/validators.fr.xlf b/translations/security/account_create/validators.fr.xlf new file mode 100644 index 0000000..5229d73 --- /dev/null +++ b/translations/security/account_create/validators.fr.xlf @@ -0,0 +1,31 @@ + + + +
+ +
+ + + + account_create_action.password.invalid_message + Les mots de passe doivent être identiques + + + + account_create.firstname.empty.error + Le prénom est obligatoire pour un compte particulier + + + + account_create.lastname.empty.error + Le nom est obligatoire pour un compte particulier + + + + account_create.name.empty.error + Le nom du lieu est obligatoire + + + +
+
diff --git a/translations/security/login/security.fr.xlf b/translations/security/login/security.fr.xlf new file mode 100644 index 0000000..b2a3320 --- /dev/null +++ b/translations/security/login/security.fr.xlf @@ -0,0 +1,46 @@ + + + +
+ +
+ + + + login_action.title + Connexion + + + + login_action.info + Lorem ipsum dolor sit amet. Est voluptatibus voluptas ut quia quidem ea ducimus. + + + + login_action.login.button + Connexion + + + + login_action.create_account.button + Créer un compte + + + + login_action.create_account.button + Mot de passe oublié + + + + login_action.fom.username.label + Adresse e-mail + + + + login_action.fom.password.label + Mot de passe + + + +
+
diff --git a/translations/security/lost_password/email.fr.xlf b/translations/security/lost_password/email.fr.xlf new file mode 100644 index 0000000..278189a --- /dev/null +++ b/translations/security/lost_password/email.fr.xlf @@ -0,0 +1,16 @@ + + + +
+ +
+ + + + lost_password.email.subject + Réinitialisation de votre mot de passe + + + +
+
diff --git a/translations/security/lost_password/messages.fr.xlf b/translations/security/lost_password/messages.fr.xlf new file mode 100644 index 0000000..e0978a2 --- /dev/null +++ b/translations/security/lost_password/messages.fr.xlf @@ -0,0 +1,21 @@ + + + +
+ +
+ + + + lost_password.form.success + Si votre email a été trouvé, un email avec un lien de réinitialisation de votre mot de passe vous été envoyé. + + + + lost_password.user_lostpassword_token_expired.exception + Le jeton de sécurité a expiré, veuillez renouveler votre demande svp. + + + +
+
diff --git a/translations/security/lost_password/security.fr.xlf b/translations/security/lost_password/security.fr.xlf new file mode 100644 index 0000000..d993597 --- /dev/null +++ b/translations/security/lost_password/security.fr.xlf @@ -0,0 +1,41 @@ + + + +
+ +
+ + + + login_action.title + Mot de passe oublié + + + + login_action.info + Entrer votre email pour recevoir un lien permettant de réinitialiser le mot de passe associé à votre compte. + + + + lost_password_action.create_account.button + Créer un compte + + + + lost_password_action.login.link + Retour à connexion + + + + lost_password.form.email.placeholder + monemail@domain.com + + + + lost_password.form.submit + Réinitialiser mon mot de passe + + + +
+
diff --git a/translations/security/reset_password/messages.fr.xlf b/translations/security/reset_password/messages.fr.xlf new file mode 100644 index 0000000..1a45a93 --- /dev/null +++ b/translations/security/reset_password/messages.fr.xlf @@ -0,0 +1,26 @@ + + + +
+ +
+ + + + reset_password.user_not_found.exception + Aucun·e utilisateur·rice correspondant à ce code n'a été trouvé·e, veuillez renouveler votre demande. + + + + reset_password.user_lostpassword_token_expired.exception + Le jeton de sécurité a expiré, veuillez renouveler votre demande svp. + + + + reset_password.form.success + Votre mot de passe a bien été réinitialisé, vous pouvez désormais vous connecter + + + +
+
diff --git a/translations/security/reset_password/security.fr.xlf b/translations/security/reset_password/security.fr.xlf new file mode 100644 index 0000000..db861f9 --- /dev/null +++ b/translations/security/reset_password/security.fr.xlf @@ -0,0 +1,21 @@ + + + +
+ +
+ + + + reset_password_action.title + Réinitialiser le mot de passe + + + + reset_password_action.info + Veuillez entrer un nouveau mot de passe. + + + +
+
diff --git a/translations/service_request/admin.fr.xlf b/translations/service_request/admin.fr.xlf new file mode 100644 index 0000000..1032297 --- /dev/null +++ b/translations/service_request/admin.fr.xlf @@ -0,0 +1,113 @@ + + + +
+ +
+ + + + loans + Emprunts + + + + Loan + Emprunt + + + + Lender + Loueur·euse + + + + Recipient + Bénéficiaire + + + + Borrower + Emprunteur·euse + + + + Start At + Début + + + + Start At + Fin + + + + Messages Count + Message(s) + + + + NEW + à confirmer propriétaire + + + + TO_CONFIRM + à confirmer emprunteur·euse + + + + CONFIRMED + confirmé + + + + REFUSED + annulé + + + + FINISHED + terminé + + + + + loan.title + Prêt du %date% entre %lender% et %borrower% + + + + panel.lender + Prêteur·euse + + + + panel.borrower + Emprunteur·euse + + + + panel.service + Service + + + + panel.loan_information + Emprunt + + + + Conversation + Conversation + + + + + conversation.title + Conversation de l'emprunt %product% + + + +
+
diff --git a/translations/service_request/messages.fr.xlf b/translations/service_request/messages.fr.xlf new file mode 100644 index 0000000..2cfd9a1 --- /dev/null +++ b/translations/service_request/messages.fr.xlf @@ -0,0 +1,34 @@ + + + +
+ +
+ + + loan.new_action.form.message + Précisez votre demande + + + + loan.new_action.form.startAt + Date de début + + + + loan.new_action.form.endAt + Date de fin + + + + loan.new_action.form.submit + Envoyer la demande + + + + loan.new_action.form.success + Votre demande a bien été créée et le·la prêteur·euse vient d'etre alerté·e. + + +
+
diff --git a/translations/service_request/validators.fr.xlf b/translations/service_request/validators.fr.xlf new file mode 100644 index 0000000..4fbc995 --- /dev/null +++ b/translations/service_request/validators.fr.xlf @@ -0,0 +1,19 @@ + + + +
+ +
+ + + + validator.product.productavailabilitynooverlap + + Vous ne pouvez pas choisir ces dates, il existe un conflit avec les dates d'indisponibilité actuelles du produit. + Les dates d'indisponibilité sont indiquées en rouge sur le calendrier. + + + + +
+
diff --git a/translations/shared/date.fr.xlf b/translations/shared/date.fr.xlf new file mode 100644 index 0000000..1b74856 --- /dev/null +++ b/translations/shared/date.fr.xlf @@ -0,0 +1,19 @@ + + + +
+ +
+ + + format.date + d/m/Y + + + + format.time + H:i:s + + +
+
diff --git a/translations/templates/admin/group/invite/admin.fr.xlf b/translations/templates/admin/group/invite/admin.fr.xlf new file mode 100644 index 0000000..cde1ad9 --- /dev/null +++ b/translations/templates/admin/group/invite/admin.fr.xlf @@ -0,0 +1,23 @@ + + + +
+ +
+ + + templates.admin.group.invite.h1 + Envoyer une invitation pour le groupe %group% + + + + templates.admin.group.invite.p1 + + Veuillez entrer l'email de la personne ou des personnes à inviter. Ils recevront + un lien afin d'accepter l'invitation et de compléter les informatons + de leur compte. + + + +
+
diff --git a/translations/templates/components/form/photo_preview/messages.fr.xlf b/translations/templates/components/form/photo_preview/messages.fr.xlf new file mode 100644 index 0000000..0630b60 --- /dev/null +++ b/translations/templates/components/form/photo_preview/messages.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ + prefix: templates.components.form.photo_preview +
+ + + templates.components.form.photo_preview.link.delete + supprimer + + +
+
diff --git a/translations/templates/components/item/product/messages.fr.xlf b/translations/templates/components/item/product/messages.fr.xlf new file mode 100644 index 0000000..24e53bd --- /dev/null +++ b/translations/templates/components/item/product/messages.fr.xlf @@ -0,0 +1,42 @@ + + + +
+ +
+ + + templates.components.item.product.edit + Modifier + + + templates.components.item.product.duplicate + Dupliquer + + + templates.components.item.product.manage_availabilities + Gérer les disponibilités + + + templates.components.item.product.pause + Mettre en pause + + + templates.components.item.product.resume + Réactiver + + + templates.components.item.product.active + Actif + + + templates.components.item.product.paused + En pause + + + templates.components.item.product.delete + Supprimer + + +
+
diff --git a/translations/templates/components/layout/navbar/messages.fr.xlf b/translations/templates/components/layout/navbar/messages.fr.xlf new file mode 100644 index 0000000..d277dc0 --- /dev/null +++ b/translations/templates/components/layout/navbar/messages.fr.xlf @@ -0,0 +1,31 @@ + + + +
+ +
+ + + + templates.components.layout.navbar.contact + Contact + + + + templates.components.layout.navbar.logged + Connecté en tant que %name% + + + + templates.components.layout.navbar.admin_link + Espace d'administration + + + + templates.components.layout.navbar.disconnect + Me déconnecter + + + +
+
diff --git a/translations/templates/components/product/calendar/messages.fr.xlf b/translations/templates/components/product/calendar/messages.fr.xlf new file mode 100644 index 0000000..563e849 --- /dev/null +++ b/translations/templates/components/product/calendar/messages.fr.xlf @@ -0,0 +1,46 @@ + + + +
+ +
+ + + + templates.components.product.calendar.object.edit + Modifier mon objet + + + + templates.components.product.calendar.service.edit + Modifier mon service + + + + templates.components.product.calendar.service.start_date + Date de début + + + + templates.components.product.calendar.service.end_date + Date de fin + + + + templates.components.product.calendar.reset + Réinitialiser les dates + + + + templates.components.product.calendar.service_request + Envoyer ma demande + + + + templates.components.product.calendar.clarifying_sentence + Sélectionnez vos dates dans le calendrier et cliquez sur le bouton "Envoyer ma demande" pour faire une demande d'emprunt + + + +
+
diff --git a/translations/templates/components/product/lender/messages.fr.xlf b/translations/templates/components/product/lender/messages.fr.xlf new file mode 100644 index 0000000..8d0f695 --- /dev/null +++ b/translations/templates/components/product/lender/messages.fr.xlf @@ -0,0 +1,19 @@ + + + +
+ +
+ + + templates.components.product.lender.address + Adresse : + + + + templates.components.product.lender.schedule + Horaires : + + +
+
diff --git a/translations/templates/components/product/object_fields/messages.fr.xlf b/translations/templates/components/product/object_fields/messages.fr.xlf new file mode 100644 index 0000000..e779fd3 --- /dev/null +++ b/translations/templates/components/product/object_fields/messages.fr.xlf @@ -0,0 +1,19 @@ + + + +
+ +
+ + + templates.components.product.object_fields.feedback_message + Fichier "%file%" trop gros. + + + + templates.components.product.object_fields.max_images_error + Vous ne pouvez déposer que %count% images au maximum. + + +
+
diff --git a/translations/templates/components/product/search/messages.fr.xlf b/translations/templates/components/product/search/messages.fr.xlf new file mode 100644 index 0000000..a74a7fd --- /dev/null +++ b/translations/templates/components/product/search/messages.fr.xlf @@ -0,0 +1,45 @@ + + + +
+ +
+ + + templates.components.product.search.accordion.toogle + Affiner la recherche + + + + templates.components.product.search.city.label + À proximité + + + + templates.components.product.search.city.placeholder + Ville + + + + templates.components.product.search.city.tooltip + Filtrer par distance maximum d'une ville donnée + + + + templates.components.product.search.place.label + Chez un partenaire + + + + templates.components.product.search.place.label + Filtrer les objets et services d'un lieu particulier + + + + templates.components.product.search.button.reset + Réinitialiser le formulaire + + + +
+
diff --git a/translations/templates/components/product/write_message/messages.fr.xlf b/translations/templates/components/product/write_message/messages.fr.xlf new file mode 100644 index 0000000..b26ef1b --- /dev/null +++ b/translations/templates/components/product/write_message/messages.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + templates.components.product.write_message.title + Message + + +
+
diff --git a/translations/templates/email/admin/group/group_invitation/email.fr.xlf b/translations/templates/email/admin/group/group_invitation/email.fr.xlf new file mode 100644 index 0000000..fb6461a --- /dev/null +++ b/translations/templates/email/admin/group/group_invitation/email.fr.xlf @@ -0,0 +1,34 @@ + + + +
+ +
+ + + templates.email.admin.group.group_invitation.hi + Bonjour ! + + + + templates.email.admin.group.group_invitation.p1 + Vous avez reçu une invitation pour le groupe %group%. + + + + templates.email.admin.group.group_invitation.p2 + Cliquez sur le lien suivant pour accéder à la page du groupe et accepter l'invitation. + + + + templates.email.admin.group.group_invitation.cta + Accédez au groupe + + + + templates.email.admin.group.group_invitation.cta2 + Ou copiez-collez le lien ci-dessous dans votre navigateur : + + +
+
diff --git a/translations/templates/email/admin/new/new_admin/email.fr.xlf b/translations/templates/email/admin/new/new_admin/email.fr.xlf new file mode 100644 index 0000000..0c44dbe --- /dev/null +++ b/translations/templates/email/admin/new/new_admin/email.fr.xlf @@ -0,0 +1,29 @@ + + + +
+ +
+ + + app.mailer.email.admin.new.new_admin_email.subject + Confirmation de votre compte Administrateur %brand% : échanges de bien et services + + + + templates.email.admin.new.new_admin.p1 + Bonjour ! + + + + templates.email.admin.new.new_admin.p2 + Votre compte administrateur·rice sur Plateforme EBS a été créé. + + + + templates.email.admin.new.new_admin.p3 + La personne qui vous a créé votre compte vous a mis un mot de passe temporaire, pensez à le changer + + +
+
diff --git a/translations/templates/email/admin/promote_to_admin/email.fr.xlf b/translations/templates/email/admin/promote_to_admin/email.fr.xlf new file mode 100644 index 0000000..ce32984 --- /dev/null +++ b/translations/templates/email/admin/promote_to_admin/email.fr.xlf @@ -0,0 +1,24 @@ + + + +
+ +
+ + + app.mailer.email.admin.promote_to_admin.promote_to_admin_email.subject + %brand%: Changement de rôle + + + + templates.email.admin.promote_to_admin.promote_to_admin.p1 + Bonjour ! + + + + templates.email.admin.promote_to_admin.promote_to_admin.p2 + Votre êtes désormais administrateur·rice. + + +
+
diff --git a/translations/templates/email/admin/user_group/admin_promotion/email.fr.xlf b/translations/templates/email/admin/user_group/admin_promotion/email.fr.xlf new file mode 100644 index 0000000..162f2e8 --- /dev/null +++ b/translations/templates/email/admin/user_group/admin_promotion/email.fr.xlf @@ -0,0 +1,19 @@ + + + +
+ +
+ + + templates.email.admin.user_group.main_admin_promotion.h1 + Bonjour ! + + + + templates.email.admin.user_group.main_admin_promotion.p1 + Vous êtes maintenant administrateur·rice principal·e du groupe %group% + + +
+
diff --git a/translations/templates/email/admin/user_group/main_admin_promotion/email.fr.xlf b/translations/templates/email/admin/user_group/main_admin_promotion/email.fr.xlf new file mode 100644 index 0000000..fe834d5 --- /dev/null +++ b/translations/templates/email/admin/user_group/main_admin_promotion/email.fr.xlf @@ -0,0 +1,19 @@ + + + +
+ +
+ + + templates.email.admin.user_group.change_role_to_admin.h1 + Bonjour ! + + + + templates.email.admin.user_group.admin_promotion.p1 + Vous êtes maintenant administrateur·rice du groupe %group% + + +
+
diff --git a/translations/templates/email/command/end_membership/email.fr.xlf b/translations/templates/email/command/end_membership/email.fr.xlf new file mode 100644 index 0000000..57d81a3 --- /dev/null +++ b/translations/templates/email/command/end_membership/email.fr.xlf @@ -0,0 +1,20 @@ + + + +
+ +
+ + + emplates.email.command.end_membership.h1 + Bonjour ! + + + + templates.email.command.end_membership.p1 + Votre adhésion au groupe %group% est terminée. + + + +
+
diff --git a/translations/templates/email/command/notify_membership_expiration/email.fr.xlf b/translations/templates/email/command/notify_membership_expiration/email.fr.xlf new file mode 100644 index 0000000..3f2de20 --- /dev/null +++ b/translations/templates/email/command/notify_membership_expiration/email.fr.xlf @@ -0,0 +1,20 @@ + + + +
+ +
+ + + templates.email.command.notify_membership_expiration.h1 + Bonjour ! + + + + templates.email.command.notify_membership_expiration.p1 + Votre adhésion au groupe %group% expire dans %days% jour(s). + + + +
+
diff --git a/translations/templates/email/command/notify_service_request_end/email.fr.xlf b/translations/templates/email/command/notify_service_request_end/email.fr.xlf new file mode 100644 index 0000000..7e69b58 --- /dev/null +++ b/translations/templates/email/command/notify_service_request_end/email.fr.xlf @@ -0,0 +1,20 @@ + + + +
+ +
+ + + templates.email.command.notify_service_request_end.h1 + Bonjour ! + + + + templates.email.command.notify_service_request_end.p1 + Fin de la transaction concernant "%product%" demain %date%. + + + +
+
diff --git a/translations/templates/email/command/notify_service_request_start/email.fr.xlf b/translations/templates/email/command/notify_service_request_start/email.fr.xlf new file mode 100644 index 0000000..5652889 --- /dev/null +++ b/translations/templates/email/command/notify_service_request_start/email.fr.xlf @@ -0,0 +1,20 @@ + + + +
+ +
+ + + templates.email.command.notify_service_request_start.h1 + Bonjour ! + + + + templates.email.command.notify_service_request_start.p1 + Début de la transaction concernant "%product%" demain %date%. + + + +
+
diff --git a/translations/templates/email/security/create_account_step1/email.fr.xlf b/translations/templates/email/security/create_account_step1/email.fr.xlf new file mode 100644 index 0000000..9ae54d3 --- /dev/null +++ b/translations/templates/email/security/create_account_step1/email.fr.xlf @@ -0,0 +1,39 @@ + + + +
+ +
+ + + templates.email.security.create_account_step1.hi + Bonjour ! + + + + templates.email.security.create_account_step1.invitation.p1 + Vous avez reçu une invitation pour le groupe %group%. + + + + templates.email.security.create_account_step1.invitation.p2 + Une fois votre compte confirmé, vous serez redirigé·e vers la page du groupe afin de pouvoir accepter l'invitation. + + + + templates.email.security.create_account_step1.p1 + Cliquez sur le lien suivant pour confirmer votre compte : + + + + templates.email.security.create_account_step1.cta + Confirmer mon compte + + + + templates.email.security.create_account_step1.cta2 + Ou copiez-collez le lien ci-dessous dans votre navigateur : + + +
+
diff --git a/translations/templates/email/service_request/accepted/email.fr.xlf b/translations/templates/email/service_request/accepted/email.fr.xlf new file mode 100644 index 0000000..8646ab4 --- /dev/null +++ b/translations/templates/email/service_request/accepted/email.fr.xlf @@ -0,0 +1,30 @@ + + + +
+ + prefix: email.service_request.accepted +
+ + + templates.email.service_request.accepted.hi + Bonjour ! + + + + templates.email.service_request.accepted.p1 + Votre demande de service du %date% (%product_name%) a été acceptée par %owner_display_name%. + + + + templates.email.service_request.accepted.p2 + Vous devez confirmer à votre tour afin de finaliser la demande. + + + + templates.email.service_request.accepted.cta + Accéder à la conversation de la demande. + + +
+
diff --git a/translations/templates/email/service_request/confirmed/email.fr.xlf b/translations/templates/email/service_request/confirmed/email.fr.xlf new file mode 100644 index 0000000..f5b6275 --- /dev/null +++ b/translations/templates/email/service_request/confirmed/email.fr.xlf @@ -0,0 +1,30 @@ + + + +
+ + prefix: templates.email.service_request.confirmed +
+ + + templates.email.service_request.confirmed.hi + Bonjour ! + + + + templates.email.service_request.confirmed.p1 + La demande de service du %date% (%product_name%) a été confirmée par %recipient_display_name%. + + + + templates.email.service_request.confirmed.p2 + Vous pouvez désormais échanger afin de plannifier votre rendez-vous. + + + + templates.email.service_request.confirmed.cta + Accéder à la conversation de la demande. + + +
+
diff --git a/translations/templates/email/service_request/modified_by/email.fr.xlf b/translations/templates/email/service_request/modified_by/email.fr.xlf new file mode 100644 index 0000000..2e976ad --- /dev/null +++ b/translations/templates/email/service_request/modified_by/email.fr.xlf @@ -0,0 +1,35 @@ + + + +
+ + prefix: email.service_request.modified_by +
+ + + templates.email.service_request.modified_by.hi + Bonjour ! + + + + templates.email.service_request.modified_by.p1 + La demande de service du %date% (%product_name%) a été modifiée par %modified_by%. + + + + templates.email.service_request.modified_by.p2 + Nouvelles dates proposées : du %startAt% au %endAt%. + + + + templates.email.service_request.modified_by.p3 + Vous devez valider les nouvelles dates proposées afin de finaliser la demande. + + + + templates.email.service_request.modified_by.cta + Accéder à la conversation de la demande. + + +
+
diff --git a/translations/templates/email/service_request/refused/email.fr.xlf b/translations/templates/email/service_request/refused/email.fr.xlf new file mode 100644 index 0000000..da98332 --- /dev/null +++ b/translations/templates/email/service_request/refused/email.fr.xlf @@ -0,0 +1,25 @@ + + + +
+ + prefix: templates.email.service_request.refused +
+ + + templates.email.service_request.refused.hi + Bonjour ! + + + + templates.email.service_request.refused.p1 + La demande d'emprunt du %date% (%product_name%) a été annulée par %actor%. + + + + templates.email.service_request.refused.cta + Accéder à la conversation de la demande. + + +
+
diff --git a/translations/templates/layout/base/messages.fr.xlf b/translations/templates/layout/base/messages.fr.xlf new file mode 100644 index 0000000..aa92463 --- /dev/null +++ b/translations/templates/layout/base/messages.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + templates.layout.base.notification.error + Une erreur est survenue, veuillez rééssayer plus tard + + +
+
diff --git a/translations/templates/pages/account/conversation/messages.fr.xlf b/translations/templates/pages/account/conversation/messages.fr.xlf new file mode 100644 index 0000000..519f051 --- /dev/null +++ b/translations/templates/pages/account/conversation/messages.fr.xlf @@ -0,0 +1,139 @@ + + + +
+ + prefix: templates.pages.account.conversation +
+ + + templates.pages.account.conversation.modal.edit.title + Confirmer l'emprunt + + + + templates.pages.account.conversation.modal.confirm.owner + Souhaitez-vous confirmer l'emprunt suivant ? L'emprunteur·euse devra ensuite confirmer à son tour. + + + + templates.pages.account.conversation.modal.confirm.recipient + Souhaitez-vous confirmer l'emprunt suivant ? Cela validera votre demande définitivement. + + + + templates.pages.account.conversation.modal.edit.save + Enregistrer et confirmer l'emprunt + + + + templates.pages.account.conversation.modal.edit.save_owner + Enregistrer + + + + templates.pages.account.conversation.modal.edit.cancel + Annuler + + + + templates.pages.account.conversation.modal.edit.back + Retour + + + + templates.pages.account.conversation.modal.edit.cancel + Êtes-vous sûr·e de vouloir annuler l’emprunt ? + + + + templates.pages.account.conversation.date + Le %date% + + + + templates.pages.account.conversation.dates + Du %startAt% au %endAt% + + + + templates.pages.account.conversation.messages + Messages + + + + templates.pages.account.conversation.system_message + Message automatique + + + + templates.pages.account.conversation.textarea.placeholder + Votre message + + + + templates.pages.account.conversation.confirmed_text + + La demande est bien confirmée des deux côtés. + Nous vous invitons à échanger vos coordonnées pour votre rendez vous du %startAt%. + + + + + templates.pages.account.conversation.accept_text + En attente de confirmation du·de la propriétaire. + + + + templates.pages.account.conversation.to_confirm_text + En attente de confirmation de l'emprunteur·euse. + + + + templates.pages.account.conversation.link.refuse_modal + Annuler l'emprunt + + + + templates.pages.account.conversation.link.refuse + Annuler l'emprunt + + + + templates.pages.account.conversation.link.refresh + Rafraîchir pour voir les nouveaux messages + + + + + templates.pages.account.conversation.refused_text + Cette transaction a été annulée et ne peut pas se poursuivre. + + + + templates.pages.account.conversation.link.confirm_modal + Confirmer l'emprunt + + + + templates.pages.account.conversation.link.confirm + Confirmer + + + + templates.pages.account.conversation.link.finalize + Finaliser l'emprunt + + + + templates.pages.account.conversation.link.finalize_modal + Terminer l'emprunt + + + + templates.pages.account.conversation.finalize_text + Êtes-vous sûr·e de vouloir terminer l’emprunt ? Votre article sera à nouveau disponible à partir de la date d'aujourd'hui. + + +
+
diff --git a/translations/templates/pages/account/index/messages.fr.xlf b/translations/templates/pages/account/index/messages.fr.xlf new file mode 100644 index 0000000..bb3c94e --- /dev/null +++ b/translations/templates/pages/account/index/messages.fr.xlf @@ -0,0 +1,91 @@ + + + +
+ +
+ + + templates.pages.account.index.my_account + Mon compte + + + templates.pages.account.index.vacation_mode_activate + Activer le mode vacances + + + templates.pages.account.index.disconnect + Me déconnecter + + + templates.pages.account.index.vacation_mode_desactivate + Désactiver le mode vacances + + + + templates.pages.account.index.disconnect + Me déconnecter + + + + templates.pages.account.index.create_group + Créer un groupe + + + + templates.pages.account.index.creation_reques + Demande de création de groupe + + + + + templates.pages.account.index.mail.subject + PlatformCoop : demande de création de groupe + + + + templates.pages.account.index.mail.mail_adress + Adresse e-mail du·de la gérant·e: + + + + templates.pages.account.index.mail.group + Nom du groupe: + + + + templates.pages.account.index.mail.type + Type : public ou privé + + + + templates.pages.account.index.mail.membership + Tarif : gratuit ou payant + + + + templates.pages.account.index.mail.info + Informations complémentaires: + + + templates.pages.account.index.no-address-title + Ajoutez une adresse + + + + templates.pages.account.index.no-address-message + Pour pouvoir créer un %product%, commencez par remplir votre adresse. + + + + templates.pages.account.index.no-address-cancel + Annuler + + + + templates.pages.account.index.no-address-add + Ajoutez une adresse + + +
+
diff --git a/translations/templates/pages/account/lendings/list/messages.fr.xlf b/translations/templates/pages/account/lendings/list/messages.fr.xlf new file mode 100644 index 0000000..027e31b --- /dev/null +++ b/translations/templates/pages/account/lendings/list/messages.fr.xlf @@ -0,0 +1,29 @@ + + + +
+ +
+ + + templates.pages.account.lendings.list.title + Mes prêts + + + + templates.pages.account.lendings.list.filterBy + Filtrer par produits: + + + + templates.pages.account.lendings.search + Rechercher un produit... + + + + templates.pages.account.lendings.list.no_result + Vous n'avez aucun prêt pour le moment + + +
+
diff --git a/translations/templates/pages/account/loans/list/messages.fr.xlf b/translations/templates/pages/account/loans/list/messages.fr.xlf new file mode 100644 index 0000000..e722558 --- /dev/null +++ b/translations/templates/pages/account/loans/list/messages.fr.xlf @@ -0,0 +1,34 @@ + + + +
+ +
+ + + templates.pages.account.loans.list.title + Mes emprunts + + + + templates.pages.account.loans.list.filterBy + Filtrer par produits: + + + + templates.pages.account.loans.list.no_result + Vous n'avez aucun emprunt pour le moment + + + + templates.pages.account.loans.list.link + Accéder à la conversation + + + + templates.pages.account.loans.list.new_messages + Nouveaux messages + + +
+
diff --git a/translations/templates/pages/account/loans/new/messages.fr.xlf b/translations/templates/pages/account/loans/new/messages.fr.xlf new file mode 100644 index 0000000..e293e54 --- /dev/null +++ b/translations/templates/pages/account/loans/new/messages.fr.xlf @@ -0,0 +1,18 @@ + + + +
+ +
+ + + templates.pages.account.loans.new.back_to_article + Retour à l'article + + + templates.pages.account.loans.new.title + Demande d'emprunt + + +
+
diff --git a/translations/templates/pages/account/product/list/messages.fr.xlf b/translations/templates/pages/account/product/list/messages.fr.xlf new file mode 100644 index 0000000..b4b6f33 --- /dev/null +++ b/translations/templates/pages/account/product/list/messages.fr.xlf @@ -0,0 +1,78 @@ + + + +
+ +
+ + + templates.pages.account.product.list.object.title + Mes objets + + + templates.pages.account.product.list.service.title + Mes services + + + templates.pages.account.product.list.service.filter_by + Filtrer par : + + + templates.pages.account.product.list.object.no_result + Vous n'avez aucun objet pour le moment + + + templates.pages.account.product.list.object.new + Créer un objet + + + templates.pages.account.product.list.service.no_result + Vous n'avez aucun service pour le moment + + + templates.pages.account.product.list.service.new + Créer un service + + + templates.pages.account.product.list.category + Toutes les catégories + + + templates.pages.account.product.list.notification.active_success + L'élément a bien été réactivé + + + templates.pages.account.product.list.notification.paused_success + L'élément a bien été mis en pause + + + select_placeholder + Toutes les catégories + + + templates.pages.account.product.list.no-address + Ajoutez une adresse + + + + templates.pages.account.product.list.no-address-title + Ajoutez une adresse + + + + templates.pages.account.product.list.no-address-message + Pour pouvoir créer un %product%, commencez par remplir votre adresse. + + + + templates.pages.account.product.list.no-address-cancel + Annuler + + + + templates.pages.account.product.list.no-address-add + Ajoutez une adresse + + +
+
diff --git a/translations/templates/pages/group/create/messages.fr.xlf b/translations/templates/pages/group/create/messages.fr.xlf new file mode 100644 index 0000000..5776265 --- /dev/null +++ b/translations/templates/pages/group/create/messages.fr.xlf @@ -0,0 +1,51 @@ + + + +
+ +
+ + + + templates.pages.group.create.title + Demande de création de groupe + + + + templates.pages.group.create.form.email + Adresse e-mail du·de la gérant·e + + + + templates.pages.group.create.form.name + Nom du groupe + + + + templates.pages.group.create.form.type + Type + + + + templates.pages.group.create.form.membership + Tarif + + + + templates.pages.group.create.form.subgroup + Sous-groupe de + + + + templates.pages.group.create.form.textarea + Précisez votre demande + + + + templates.pages.group.create.form.textarea + Créer le groupe + + + +
+
diff --git a/translations/templates/pages/group/list/messages.fr.xlf b/translations/templates/pages/group/list/messages.fr.xlf new file mode 100644 index 0000000..bf057aa --- /dev/null +++ b/translations/templates/pages/group/list/messages.fr.xlf @@ -0,0 +1,35 @@ + + + +
+ +
+ + + templates.pages.group.list.title + Chercher un groupe + + + + templates.pages.group.list.placeholder + Rechercher un groupe + + + + templates.pages.group.list.join + Rejoindre + + + + templates.pages.group.list.pill_public + Public + + + + templates.pages.group.list.pill_private + Privé + + + +
+
diff --git a/translations/templates/pages/group/members/messages.fr.xlf b/translations/templates/pages/group/members/messages.fr.xlf new file mode 100644 index 0000000..97f10f6 --- /dev/null +++ b/translations/templates/pages/group/members/messages.fr.xlf @@ -0,0 +1,29 @@ + + + +
+ +
+ + + templates.pages.group.members.title + Annuaire du groupe + + + + templates.pages.group.members.pending_invitation + Invitation en attente + + + + templates.pages.group.members.no_result + Aucun membre pour le moment. + + + + templates.pages.group.members.manage_button + Administrer le groupe + + +
+
diff --git a/translations/templates/pages/group/show/messages.fr.xlf b/translations/templates/pages/group/show/messages.fr.xlf new file mode 100644 index 0000000..cacb1c0 --- /dev/null +++ b/translations/templates/pages/group/show/messages.fr.xlf @@ -0,0 +1,139 @@ + + + +
+ +
+ + + templates.pages.group.show.name + Nom + + + + templates.pages.group.show.description + Description + + + + templates.pages.group.show.type + Visibilité + + + + templates.pages.group.show.type + URL + + + + templates.pages.group.show.group_join.form.submit + Rejoindre ce groupe + + + + templates.pages.group.show.payment_prepare.form.submit + Accéder au paiement + + + + templates.pages.group.show.form.accept_invitation.submit + Accepter l'invitation et rejoindre le groupe + + + + templates.pages.group.show.form.login_text + Veuillez vous logger pour rejoindre ce groupe + + + + templates.pages.group.show.form.already_member + Vous êtes membre de ce groupe. + + + + templates.pages.group.show.form.quit_group.submit + Quitter le groupe + + + + templates.pages.group.show.form.quit_group.submit-modal + Quitter le groupe + + + + templates.pages.group.show.form.quit_group.submit-modal-message + Êtes-vous sûr·e de vouloir quitter ce groupe ? + + + + templates.pages.group.show.form.quit_group.submit-modal-message-item + Vous avez des produits dans ce groupe, qu'en faisons nous: + + + + templates.pages.group.show.form.quit_group.submit-modal-item-break + Mettre les articles en pause + + + + templates.pages.group.show.form.quit_group.submit-modal-item-public + Mettre les articles en public + + + + templates.pages.group.show.type.yearly + Adhésion annuel + + + + templates.pages.group.show.type.monthly + Adhésion mensuel + + + + templates.pages.group.show.type.oneshot + Unique + + + + templates.pages.group.show.show_members + Voir les membres + + + + templates.pages.group.show.choose_offer + Rejoindre le groupe + + + + templates.pages.group.show.admin + Administrer le groupe + + + + templates.pages.group.show.close + Annuler + + + + templates.pages.group.show.no_offer + Aucune offre pour ce groupe. + + + + templates.pages.group.show.membership + Adhésion: + + + + templates.pages.group.show.membership.start + À partir de + + + + templates.pages.group.show.membership_valid_until + Adhésion valide jusqu'au %endAt%. + + +
+
diff --git a/translations/templates/pages/group/user/messages.fr.xlf b/translations/templates/pages/group/user/messages.fr.xlf new file mode 100644 index 0000000..4135afb --- /dev/null +++ b/translations/templates/pages/group/user/messages.fr.xlf @@ -0,0 +1,19 @@ + + + +
+ +
+ + + templates.pages.group.user.back_group + Retour au groupe + + + + templates.pages.group.user.no_result + %name% n'a aucun produit pour le moment. + + +
+
diff --git a/translations/templates/pages/page/messages.fr.xlf b/translations/templates/pages/page/messages.fr.xlf new file mode 100644 index 0000000..9020516 --- /dev/null +++ b/translations/templates/pages/page/messages.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + templates.cms.page.h1 + Bienvenue sur %brand% ! + + +
+
diff --git a/translations/templates/pages/product/new_object/messages.fr.xlf b/translations/templates/pages/product/new_object/messages.fr.xlf new file mode 100644 index 0000000..46072f0 --- /dev/null +++ b/translations/templates/pages/product/new_object/messages.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + templates.pages.product.new_object.title + Créer un objet + + +
+
diff --git a/translations/templates/pages/product/new_service/messages.fr.xlf b/translations/templates/pages/product/new_service/messages.fr.xlf new file mode 100644 index 0000000..7175a4e --- /dev/null +++ b/translations/templates/pages/product/new_service/messages.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + templates.pages.product.new_service.title + Créer un service + + +
+
diff --git a/translations/templates/pages/product/product_availability/messages.fr.xlf b/translations/templates/pages/product/product_availability/messages.fr.xlf new file mode 100644 index 0000000..0882ca7 --- /dev/null +++ b/translations/templates/pages/product/product_availability/messages.fr.xlf @@ -0,0 +1,34 @@ + + + +
+ +
+ + + templates.pages.product.product_availability.subtitle + Disponibilité + + + + templates.pages.product.product_availability.title + Ajouter une indisponibilité + + + + templates.pages.product.product_availability.date + Du %startAt% au %endAt% + + + + templates.pages.product.product_availability.delete_title + Supprimer une indisponibilité + + + + templates.pages.product.product_availability.back_to_list + Retour + + +
+
diff --git a/translations/templates/pages/product/show/messages.fr.xlf b/translations/templates/pages/product/show/messages.fr.xlf new file mode 100644 index 0000000..7a5f7a3 --- /dev/null +++ b/translations/templates/pages/product/show/messages.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + templates.pages.product.show.back_to_search + Retour à la recherche + + +
+
diff --git a/translations/templates/pages/register/step2/security.fr.xlf b/translations/templates/pages/register/step2/security.fr.xlf new file mode 100644 index 0000000..7428abc --- /dev/null +++ b/translations/templates/pages/register/step2/security.fr.xlf @@ -0,0 +1,19 @@ + + + +
+ +
+ + + templates.pages.register.step2.group_invitations + Vous avez été invité·e aux groupes : %groups% + + + + templates.pages.register.step2.info + Veuillez choisir votre type de compte et compléter vos informations. + + +
+
diff --git a/translations/templates/pages/user/change_password/messages.fr.xlf b/translations/templates/pages/user/change_password/messages.fr.xlf new file mode 100644 index 0000000..d531d09 --- /dev/null +++ b/translations/templates/pages/user/change_password/messages.fr.xlf @@ -0,0 +1,40 @@ + + + +
+ +
+ + + templates.pages.user.account.change_password.title + Changer mon mot de passe + + + + templates.pages.user.account.change_password.old + Mot de passe actuel + + + + templates.pages.user.account.change_password.new + Nouveau mot de passe + + + + templates.pages.user.account.change_password.confirm + Confirmer le nouveau mot de passe + + + + templates.pages.user.account.change_password.submit + Enregistrer + + + + templates.pages.user.account.change_password.help + 8 caractères minimum + + + +
+
diff --git a/translations/templates/pages/user/edit_profile/messages.fr.xlf b/translations/templates/pages/user/edit_profile/messages.fr.xlf new file mode 100644 index 0000000..e8323c7 --- /dev/null +++ b/translations/templates/pages/user/edit_profile/messages.fr.xlf @@ -0,0 +1,76 @@ + + + +
+ +
+ + + templates.pages.user.account.edit_profile.title + Mon compte + + + + templates.pages.user.account.edit_profile.firstname + Prénom + + + + templates.pages.user.account.edit_profile.lastname + Nom + + + + templates.pages.user.account.edit_profile.image + Image + + + + templates.pages.user.account.edit_profile.description + Description + + + + templates.pages.user.account.edit_profile.category + Catégorie préférée + + + + templates.pages.user.account.edit_profile.phone + Téléphone + + + + templates.pages.user.account.edit_profile.sms + Activer les notifications par SMS + + + + templates.pages.user.account.edit_profile.submit + Enregistrer + + + + + templates.pages.user.account.edit_profile.name + Nom + + + + templates.pages.user.account.edit_profile.schedule + Horaires + + + + templates.pages.user.account.edit_profile.service + Services + + + + templates.pages.user.account.edit_profile.object + Objets + + + +
+
diff --git a/translations/templates/pages/user/profile/messages.fr.xlf b/translations/templates/pages/user/profile/messages.fr.xlf new file mode 100644 index 0000000..58b0917 --- /dev/null +++ b/translations/templates/pages/user/profile/messages.fr.xlf @@ -0,0 +1,15 @@ + + + +
+ +
+ + + templates.pages.user.account.profile.no_result + Aucun produit pour l'instant. + + + +
+
diff --git a/translations/templates/twig/exception/error/messages.fr.xlf b/translations/templates/twig/exception/error/messages.fr.xlf new file mode 100644 index 0000000..85578f4 --- /dev/null +++ b/translations/templates/twig/exception/error/messages.fr.xlf @@ -0,0 +1,29 @@ + + + +
+ +
+ + + templates.twig.exception.error.h5 + Oops ! Une erreur %status_code% est survenue ! + + + + templates.twig.exception.error.p1 + Désolé mais une erreur "%status_text%" (code %status_code%) est survenue. + + + + templates.twig.exception.error.403 + Accès non autorisé. + + + + templates.twig.exception.error.404 + Page non trouvée. + + +
+
diff --git a/translations/templates/user/account/messages.fr.xlf b/translations/templates/user/account/messages.fr.xlf new file mode 100644 index 0000000..0f129a9 --- /dev/null +++ b/translations/templates/user/account/messages.fr.xlf @@ -0,0 +1,30 @@ + + + +
+ +
+ + + templates.pages.user.account.change_login.form.email + Adresse e-mail + + + templates.pages.user.account.change_login.form.email_repeat + Confirmer l'adresse e-mail + + + templates.pages.user.account.change_login.form.email_placeholder + monemail@domain.com + + + templates.pages.user.account.change_login.form.submit + Enregistrer + + + templates.pages.user.account.change_login.form.success + L'e-mail a bien été modifié + + +
+
diff --git a/translations/templates/user/group/list/messages.fr.xlf b/translations/templates/user/group/list/messages.fr.xlf new file mode 100644 index 0000000..39cf3fd --- /dev/null +++ b/translations/templates/user/group/list/messages.fr.xlf @@ -0,0 +1,74 @@ + + + +
+ +
+ + + templates.pages.user.group.list.title + Mes groupes + + + + templates.pages.user.group.list.user_invitations + Mes invitations + + + + templates.pages.user.group.list.user_invitations_empty + Pas d'invitation en cours. + + + + templates.pages.user.group.list.no_group + Vous n'avez aucun groupe pour le moment. + + + + templates.pages.user.group.list.user_no_group + Vous n'avez aucun groupe pour le moment. + + + + templates.pages.user.group.list.user_show_all_group + Voir tous les groupes + + + + templates.pages.user.group.list.user_groups + Mes groupes + + + + templates.pages.user.group.list.join + Rejoindre + + + + templates.pages.user.group.list.admin + Administrer le groupe + + + + templates.pages.user.group.list.join.form.submit + ]]> + + + + templates.pages.user.group.list.public + Public + + + + templates.pages.user.group.list.join.private + Privé + + + + templates.pages.user.group.list.join.all_group + Voir tous les groupes + + +
+
diff --git a/translations/user/address/messages.fr.xlf b/translations/user/address/messages.fr.xlf new file mode 100644 index 0000000..f3331e9 --- /dev/null +++ b/translations/user/address/messages.fr.xlf @@ -0,0 +1,93 @@ + + + +
+ +
+ + + + + address.step1_action.title + Mon adresse + + + + address.step1_action.plaintext + Cette adresse sera valable pour tous vos objets et services. Saisissez votre adresse puis cliquez sur vérifier mon adresse + + + + address.step1_action.form.address + Adresse + + + + address.step1_action.form.address_supplement + Complément d'adresse + + + + address.step1_action.form.postal_code + Code postal + + + + address.step1_action.form.locality + Ville + + + + address.step1_action.form.country + Pays + + + + address.step1_action.form.submit + Vérifier l'adresse + + + + address.step1_action.no_address.warning + Aucune adresse trouvée correspondant à ces informations, veuillez modifier votre saisie. + + + + + address.step2_action.title + Mon adresse + + + + address.step2_action.user_input + Adresse que vous avez renseignée + + + + address.step2_action.confirm_title + Veuillez confirmer votre adresse + + + + address.step2_action.confirm_intro + Sélectionnez l’adresse la plus proche + + + + address.step2_action.from.back + Modifier l'adresse + + + + address.step2_action.form.success + Votre adresse a bien été enregistrée + + + + address.step2_action.form.check_map + Vérifier sur la carte + + + +
+
diff --git a/translations/user/admin.fr.xlf b/translations/user/admin.fr.xlf new file mode 100644 index 0000000..65d82e7 --- /dev/null +++ b/translations/user/admin.fr.xlf @@ -0,0 +1,139 @@ + + + +
+ +
+ + + User + Utilisateur·rice + + + + Email + Email + + + + User Link + Accéder à la fiche de l'utilisateur·rice + + + + ADMIN + Administrateur·rice + + + + USER + Utilisateur·rice + + + + PLACE + Lieu + + + + Firstname + Prénom + + + + Lastname + Nom + + + + Avatar + Photo + + + + Phone Number + Numéro de téléphone + + + + Phone + Numéro de téléphone + + + + Schedule + Horaires + + + + Description + Description + + + + Category + Catégorie + + + + Sms Notifications + Notifications SMS + + + + Vacation Mode + Mode vacances + + + + Password + Mot de passe + + + + Plain Password + Mot de passe + + + + First + Mot de passe + + + + Second + Répéter le mot de passe + + + + Login At + Date de dernier login + + + + action.connectAs + Se connecter en tant que + + + + Export + Exporter + + + + flash.warning.connectAs + Attention, vous êtes désormais connecté·e en tant que %target_user%. + + + + action.promoteToAdmin + Passer en admin + + + + flash.success.promoteToAdmin + %target_user% est désormais administrateur·rice + + +
+
diff --git a/translations/user/change_login/messages.fr.xlf b/translations/user/change_login/messages.fr.xlf new file mode 100644 index 0000000..13e1b94 --- /dev/null +++ b/translations/user/change_login/messages.fr.xlf @@ -0,0 +1,14 @@ + + + +
+ +
+ + + change_login.action.title + Changer mon adresse e-mail + + +
+
diff --git a/translations/user/conversation/messages.fr.xlf b/translations/user/conversation/messages.fr.xlf new file mode 100644 index 0000000..6c2c1f6 --- /dev/null +++ b/translations/user/conversation/messages.fr.xlf @@ -0,0 +1,26 @@ + + + +
+ +
+ + + + new_message.form.message + Message + + + + new_message.form.message.placeholder + Écrivez un message + + + + new_message.form.submit + ]]> + + + +
+
diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf new file mode 100644 index 0000000..dd34f11 --- /dev/null +++ b/translations/validators.fr.xlf @@ -0,0 +1,22 @@ + + + +
+ +
+ + + + + validator.extensions_message + L'extension de l'image est invalide {{ extension }}. Extensions autorisées {{ extensions }}. + + + + validator.upload_maxsize_by_file.error + Chaque fichier doit faire moins de un mégaoctet. + + + +
+
diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..fef2287 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,76 @@ +const Encore = require('@symfony/webpack-encore') + +// Manually configure the runtime environment if not already configured yet by the "encore" command. +// It's useful when you use tools that rely on webpack.config.js file. +if (!Encore.isRuntimeEnvironmentConfigured()) { + Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev') +} + +Encore + // directory where compiled assets will be stored + .setOutputPath('public/build/') + // public path used by the web server to access the output path + .setPublicPath('/build') + // only needed for CDN's or subdirectory deploy + //.setManifestKeyPrefix('build/') + + /* + * ENTRY CONFIG + * + * Each entry will result in one JavaScript file (e.g. app.js) + * and one CSS file (e.g. app.css) if your JavaScript imports CSS. + */ + .addEntry('app', './assets/app.js') + + // enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js) + .enableStimulusBridge('./assets/controllers.json') + + // When enabled, Webpack "splits" your files into smaller pieces for greater optimization. + .splitEntryChunks() + + // will require an extra script tag for runtime.js + // but, you probably want this, unless you're building a single-page app + .enableSingleRuntimeChunk() + + /* + * FEATURE CONFIG + * + * Enable & configure other features below. For a full + * list of features, see: + * https://symfony.com/doc/current/frontend.html#adding-more-features + */ + .cleanupOutputBeforeBuild() + .enableBuildNotifications() + .enableSourceMaps(!Encore.isProduction()) + // enables hashed filenames (e.g. app.abc123.css) + .enableVersioning(Encore.isProduction()) + + // configure Babel + // .configureBabel((config) => { + // config.plugins.push('@babel/a-babel-plugin'); + // }) + + // enables and configure @babel/preset-env polyfills + .configureBabelPresetEnv((config) => { + config.useBuiltIns = 'usage' + config.corejs = '3.23' + }) + + // enables Sass/SCSS support + .enableSassLoader() + + // uncomment if you use TypeScript + //.enableTypeScriptLoader() + + // uncomment if you use React + //.enableReactPreset() + + // uncomment to get integrity="..." attributes on your script & link tags + // requires WebpackEncoreBundle 1.4 or higher + //.enableIntegrityHashes(Encore.isProduction()) + + // uncomment if you're having problems with a jQuery plugin + //.autoProvidejQuery() + + +module.exports = Encore.getWebpackConfig() diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..548b498 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,4800 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" + integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== + dependencies: + "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1", "@babel/compat-data@^7.20.5": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.0.tgz#c241dc454e5b5917e40d37e525e2f4530c399298" + integrity sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g== + +"@babel/core@^7.17.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.0.tgz#1341aefdcc14ccc7553fcc688dd8986a2daffc13" + integrity sha512-PuxUbxcW6ZYe656yL3EAhpy7qXKq0DmYsrJLpbB8XrsCP9Nm+XCg9XFMb5vIDliPD7+U/+M+QJlH17XOcB7eXA== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.21.0" + "@babel/helper-compilation-targets" "^7.20.7" + "@babel/helper-module-transforms" "^7.21.0" + "@babel/helpers" "^7.21.0" + "@babel/parser" "^7.21.0" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.21.0" + "@babel/types" "^7.21.0" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.2" + semver "^6.3.0" + +"@babel/generator@^7.21.0", "@babel/generator@^7.21.1": + version "7.21.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.1.tgz#951cc626057bc0af2c35cd23e9c64d384dea83dd" + integrity sha512-1lT45bAYlQhFn/BHivJs43AiW2rg3/UbLyShGfF3C0KmHvO5fSghWd5kBJy30kpRRucGzXStvnnCFniCR2kXAA== + dependencies: + "@babel/types" "^7.21.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" + integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz#acd4edfd7a566d1d51ea975dff38fd52906981bb" + integrity sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw== + dependencies: + "@babel/helper-explode-assignable-expression" "^7.18.6" + "@babel/types" "^7.18.9" + +"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.0", "@babel/helper-compilation-targets@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz#a6cd33e93629f5eb473b021aac05df62c4cd09bb" + integrity sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ== + dependencies: + "@babel/compat-data" "^7.20.5" + "@babel/helper-validator-option" "^7.18.6" + browserslist "^4.21.3" + lru-cache "^5.1.1" + semver "^6.3.0" + +"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.0.tgz#64f49ecb0020532f19b1d014b03bccaa1ab85fb9" + integrity sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.21.0" + "@babel/helper-member-expression-to-functions" "^7.21.0" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/helper-replace-supers" "^7.20.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" + "@babel/helper-split-export-declaration" "^7.18.6" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.20.5": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.0.tgz#53ff78472e5ce10a52664272a239787107603ebb" + integrity sha512-N+LaFW/auRSWdx7SHD/HiARwXQju1vXTW4fKr4u5SgBUTm51OKEjKgj+cs00ggW3kEvNqwErnlwuq7Y3xBe4eg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + regexpu-core "^5.3.1" + +"@babel/helper-define-polyfill-provider@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz#8612e55be5d51f0cd1f36b4a5a83924e89884b7a" + integrity sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww== + dependencies: + "@babel/helper-compilation-targets" "^7.17.7" + "@babel/helper-plugin-utils" "^7.16.7" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + semver "^6.1.2" + +"@babel/helper-environment-visitor@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" + integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== + +"@babel/helper-explode-assignable-expression@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" + integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0", "@babel/helper-function-name@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz#d552829b10ea9f120969304023cd0645fa00b1b4" + integrity sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg== + dependencies: + "@babel/template" "^7.20.7" + "@babel/types" "^7.21.0" + +"@babel/helper-hoist-variables@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" + integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-member-expression-to-functions@^7.20.7", "@babel/helper-member-expression-to-functions@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz#319c6a940431a133897148515877d2f3269c3ba5" + integrity sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q== + dependencies: + "@babel/types" "^7.21.0" + +"@babel/helper-module-imports@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" + integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.0", "@babel/helper-module-transforms@^7.21.2": + version "7.21.2" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz#160caafa4978ac8c00ac66636cb0fa37b024e2d2" + integrity sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.19.1" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.21.2" + "@babel/types" "^7.21.2" + +"@babel/helper-optimise-call-expression@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" + integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" + integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== + +"@babel/helper-remap-async-to-generator@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519" + integrity sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-wrap-function" "^7.18.9" + "@babel/types" "^7.18.9" + +"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz#243ecd2724d2071532b2c8ad2f0f9f083bcae331" + integrity sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-member-expression-to-functions" "^7.20.7" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.20.7" + "@babel/types" "^7.20.7" + +"@babel/helper-simple-access@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" + integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== + dependencies: + "@babel/types" "^7.20.2" + +"@babel/helper-skip-transparent-expression-wrappers@^7.20.0": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz#fbe4c52f60518cab8140d77101f0e63a8a230684" + integrity sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg== + dependencies: + "@babel/types" "^7.20.0" + +"@babel/helper-split-export-declaration@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" + integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-string-parser@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" + integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== + +"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== + +"@babel/helper-validator-option@^7.18.6": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180" + integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ== + +"@babel/helper-wrap-function@^7.18.9": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz#75e2d84d499a0ab3b31c33bcfe59d6b8a45f62e3" + integrity sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q== + dependencies: + "@babel/helper-function-name" "^7.19.0" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.20.5" + "@babel/types" "^7.20.5" + +"@babel/helpers@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.0.tgz#9dd184fb5599862037917cdc9eecb84577dc4e7e" + integrity sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA== + dependencies: + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.21.0" + "@babel/types" "^7.21.0" + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.20.7", "@babel/parser@^7.21.0", "@babel/parser@^7.21.2": + version "7.21.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.2.tgz#dacafadfc6d7654c3051a66d6fe55b6cb2f2a0b3" + integrity sha512-URpaIJQwEkEC2T9Kn+Ai6Xe/02iNaVCuT/PtoRz3GPVJVDpPd7mLo+VddTbhCRU9TXqW5mSrQfXZyi8kDKOVpQ== + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" + integrity sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.9": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz#d9c85589258539a22a901033853101a6198d4ef1" + integrity sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" + "@babel/plugin-proposal-optional-chaining" "^7.20.7" + +"@babel/plugin-proposal-async-generator-functions@^7.20.1": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz#bfb7276d2d573cb67ba379984a2334e262ba5326" + integrity sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-remap-async-to-generator" "^7.18.9" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-proposal-class-properties@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" + integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-proposal-class-static-block@^7.18.6": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz#77bdd66fb7b605f3a61302d224bdfacf5547977d" + integrity sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.21.0" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-proposal-dynamic-import@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz#72bcf8d408799f547d759298c3c27c7e7faa4d94" + integrity sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-proposal-export-namespace-from@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz#5f7313ab348cdb19d590145f9247540e94761203" + integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-proposal-json-strings@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz#7e8788c1811c393aff762817e7dbf1ebd0c05f0b" + integrity sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-proposal-logical-assignment-operators@^7.18.9": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz#dfbcaa8f7b4d37b51e8bfb46d94a5aea2bb89d83" + integrity sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" + integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-proposal-numeric-separator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz#899b14fbafe87f053d2c5ff05b36029c62e13c75" + integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-proposal-object-rest-spread@^7.20.2": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a" + integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== + dependencies: + "@babel/compat-data" "^7.20.5" + "@babel/helper-compilation-targets" "^7.20.7" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.20.7" + +"@babel/plugin-proposal-optional-catch-binding@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz#f9400d0e6a3ea93ba9ef70b09e72dd6da638a2cb" + integrity sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-proposal-optional-chaining@^7.18.9", "@babel/plugin-proposal-optional-chaining@^7.20.7": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz#886f5c8978deb7d30f678b2e24346b287234d3ea" + integrity sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-proposal-private-methods@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea" + integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-proposal-private-property-in-object@^7.18.6": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz#19496bd9883dd83c23c7d7fc45dcd9ad02dfa1dc" + integrity sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-create-class-features-plugin" "^7.21.0" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-proposal-unicode-property-regex@^7.18.6", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e" + integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-import-assertions@^7.20.0": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz#bb50e0d4bea0957235390641209394e87bdb9cc4" + integrity sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ== + dependencies: + "@babel/helper-plugin-utils" "^7.19.0" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-arrow-functions@^7.18.6": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz#bea332b0e8b2dab3dafe55a163d8227531ab0551" + integrity sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-async-to-generator@^7.18.6": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz#dfee18623c8cb31deb796aa3ca84dda9cea94354" + integrity sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q== + dependencies: + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-remap-async-to-generator" "^7.18.9" + +"@babel/plugin-transform-block-scoped-functions@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz#9187bf4ba302635b9d70d986ad70f038726216a8" + integrity sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-block-scoping@^7.20.2": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz#e737b91037e5186ee16b76e7ae093358a5634f02" + integrity sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-classes@^7.20.2": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz#f469d0b07a4c5a7dbb21afad9e27e57b47031665" + integrity sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-compilation-targets" "^7.20.7" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.21.0" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-replace-supers" "^7.20.7" + "@babel/helper-split-export-declaration" "^7.18.6" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.18.9": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz#704cc2fd155d1c996551db8276d55b9d46e4d0aa" + integrity sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/template" "^7.20.7" + +"@babel/plugin-transform-destructuring@^7.20.2": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.7.tgz#8bda578f71620c7de7c93af590154ba331415454" + integrity sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz#b286b3e7aae6c7b861e45bed0a2fafd6b1a4fef8" + integrity sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-duplicate-keys@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz#687f15ee3cdad6d85191eb2a372c4528eaa0ae0e" + integrity sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-exponentiation-operator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz#421c705f4521888c65e91fdd1af951bfefd4dacd" + integrity sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-for-of@^7.18.8": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz#964108c9988de1a60b4be2354a7d7e245f36e86e" + integrity sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-function-name@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz#cc354f8234e62968946c61a46d6365440fc764e0" + integrity sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ== + dependencies: + "@babel/helper-compilation-targets" "^7.18.9" + "@babel/helper-function-name" "^7.18.9" + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-literals@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz#72796fdbef80e56fba3c6a699d54f0de557444bc" + integrity sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-member-expression-literals@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz#ac9fdc1a118620ac49b7e7a5d2dc177a1bfee88e" + integrity sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-modules-amd@^7.19.6": + version "7.20.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz#3daccca8e4cc309f03c3a0c4b41dc4b26f55214a" + integrity sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g== + dependencies: + "@babel/helper-module-transforms" "^7.20.11" + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-modules-commonjs@^7.19.6": + version "7.21.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz#6ff5070e71e3192ef2b7e39820a06fb78e3058e7" + integrity sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA== + dependencies: + "@babel/helper-module-transforms" "^7.21.2" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-simple-access" "^7.20.2" + +"@babel/plugin-transform-modules-systemjs@^7.19.6": + version "7.20.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz#467ec6bba6b6a50634eea61c9c232654d8a4696e" + integrity sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw== + dependencies: + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-module-transforms" "^7.20.11" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-validator-identifier" "^7.19.1" + +"@babel/plugin-transform-modules-umd@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz#81d3832d6034b75b54e62821ba58f28ed0aab4b9" + integrity sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ== + dependencies: + "@babel/helper-module-transforms" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.19.1": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz#626298dd62ea51d452c3be58b285d23195ba69a8" + integrity sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.20.5" + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-new-target@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz#d128f376ae200477f37c4ddfcc722a8a1b3246a8" + integrity sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-object-super@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c" + integrity sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-replace-supers" "^7.18.6" + +"@babel/plugin-transform-parameters@^7.20.1", "@babel/plugin-transform-parameters@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz#0ee349e9d1bc96e78e3b37a7af423a4078a7083f" + integrity sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-property-literals@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz#e22498903a483448e94e032e9bbb9c5ccbfc93a3" + integrity sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-regenerator@^7.18.6": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz#57cda588c7ffb7f4f8483cc83bdcea02a907f04d" + integrity sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + regenerator-transform "^0.15.1" + +"@babel/plugin-transform-reserved-words@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz#b1abd8ebf8edaa5f7fe6bbb8d2133d23b6a6f76a" + integrity sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-shorthand-properties@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz#6d6df7983d67b195289be24909e3f12a8f664dc9" + integrity sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-spread@^7.19.0": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz#c2d83e0b99d3bf83e07b11995ee24bf7ca09401e" + integrity sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" + +"@babel/plugin-transform-sticky-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz#c6706eb2b1524028e317720339583ad0f444adcc" + integrity sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-template-literals@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz#04ec6f10acdaa81846689d63fae117dd9c243a5e" + integrity sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-typeof-symbol@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz#c8cea68263e45addcd6afc9091429f80925762c0" + integrity sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-unicode-escapes@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz#1ecfb0eda83d09bbcb77c09970c2dd55832aa246" + integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-unicode-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz#194317225d8c201bbae103364ffe9e2cea36cdca" + integrity sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/preset-env@^7.16.0": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.20.2.tgz#9b1642aa47bb9f43a86f9630011780dab7f86506" + integrity sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg== + dependencies: + "@babel/compat-data" "^7.20.1" + "@babel/helper-compilation-targets" "^7.20.0" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-validator-option" "^7.18.6" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.18.9" + "@babel/plugin-proposal-async-generator-functions" "^7.20.1" + "@babel/plugin-proposal-class-properties" "^7.18.6" + "@babel/plugin-proposal-class-static-block" "^7.18.6" + "@babel/plugin-proposal-dynamic-import" "^7.18.6" + "@babel/plugin-proposal-export-namespace-from" "^7.18.9" + "@babel/plugin-proposal-json-strings" "^7.18.6" + "@babel/plugin-proposal-logical-assignment-operators" "^7.18.9" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6" + "@babel/plugin-proposal-numeric-separator" "^7.18.6" + "@babel/plugin-proposal-object-rest-spread" "^7.20.2" + "@babel/plugin-proposal-optional-catch-binding" "^7.18.6" + "@babel/plugin-proposal-optional-chaining" "^7.18.9" + "@babel/plugin-proposal-private-methods" "^7.18.6" + "@babel/plugin-proposal-private-property-in-object" "^7.18.6" + "@babel/plugin-proposal-unicode-property-regex" "^7.18.6" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.20.0" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-transform-arrow-functions" "^7.18.6" + "@babel/plugin-transform-async-to-generator" "^7.18.6" + "@babel/plugin-transform-block-scoped-functions" "^7.18.6" + "@babel/plugin-transform-block-scoping" "^7.20.2" + "@babel/plugin-transform-classes" "^7.20.2" + "@babel/plugin-transform-computed-properties" "^7.18.9" + "@babel/plugin-transform-destructuring" "^7.20.2" + "@babel/plugin-transform-dotall-regex" "^7.18.6" + "@babel/plugin-transform-duplicate-keys" "^7.18.9" + "@babel/plugin-transform-exponentiation-operator" "^7.18.6" + "@babel/plugin-transform-for-of" "^7.18.8" + "@babel/plugin-transform-function-name" "^7.18.9" + "@babel/plugin-transform-literals" "^7.18.9" + "@babel/plugin-transform-member-expression-literals" "^7.18.6" + "@babel/plugin-transform-modules-amd" "^7.19.6" + "@babel/plugin-transform-modules-commonjs" "^7.19.6" + "@babel/plugin-transform-modules-systemjs" "^7.19.6" + "@babel/plugin-transform-modules-umd" "^7.18.6" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.19.1" + "@babel/plugin-transform-new-target" "^7.18.6" + "@babel/plugin-transform-object-super" "^7.18.6" + "@babel/plugin-transform-parameters" "^7.20.1" + "@babel/plugin-transform-property-literals" "^7.18.6" + "@babel/plugin-transform-regenerator" "^7.18.6" + "@babel/plugin-transform-reserved-words" "^7.18.6" + "@babel/plugin-transform-shorthand-properties" "^7.18.6" + "@babel/plugin-transform-spread" "^7.19.0" + "@babel/plugin-transform-sticky-regex" "^7.18.6" + "@babel/plugin-transform-template-literals" "^7.18.9" + "@babel/plugin-transform-typeof-symbol" "^7.18.9" + "@babel/plugin-transform-unicode-escapes" "^7.18.10" + "@babel/plugin-transform-unicode-regex" "^7.18.6" + "@babel/preset-modules" "^0.1.5" + "@babel/types" "^7.20.2" + babel-plugin-polyfill-corejs2 "^0.3.3" + babel-plugin-polyfill-corejs3 "^0.6.0" + babel-plugin-polyfill-regenerator "^0.4.1" + core-js-compat "^3.25.1" + semver "^6.3.0" + +"@babel/preset-modules@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" + integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" + "@babel/plugin-transform-dotall-regex" "^7.4.4" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/regjsgen@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" + integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== + +"@babel/runtime@^7.8.4": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" + integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== + dependencies: + regenerator-runtime "^0.13.11" + +"@babel/template@^7.18.10", "@babel/template@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" + integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + +"@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2": + version "7.21.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.2.tgz#ac7e1f27658750892e815e60ae90f382a46d8e75" + integrity sha512-ts5FFU/dSUPS13tv8XiEObDu9K+iagEKME9kAbaP7r0Y9KtZJZ+NGndDvWoRAYNpeWafbpFeki3q9QoMD6gxyw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.21.1" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.21.0" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.21.2" + "@babel/types" "^7.21.2" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.4.4": + version "7.21.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.2.tgz#92246f6e00f91755893c2876ad653db70c8310d1" + integrity sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw== + dependencies: + "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@eslint/eslintrc@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.0.tgz#943309d8697c52fc82c076e90c1c74fbbe69dbff" + integrity sha512-fluIaaV+GyV24CCu/ggiHdV+j4RNh85yQnAYS/G2mZODZgGmmlrgCydjUcV3YvxCm9x8nMAfThsqTni4KiXT4A== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.4.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.35.0": + version "8.35.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.35.0.tgz#b7569632b0b788a0ca0e438235154e45d42813a7" + integrity sha512-JXdzbRiWclLVoD8sNUjR443VVlYqiYmDVT6rGUEIEHU5YJW0gaVZwV2xgM7D4arkvASqD0IlLUVjHiFuxaftRw== + +"@fortawesome/fontawesome-free@^6.2.1": + version "6.3.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.3.0.tgz#b5877182692a6f7a39d1108837bec24247ba4bd7" + integrity sha512-qVtd5i1Cc7cdrqnTWqTObKQHjPWAiRwjUPaXObaeNPcy7+WKxJumGBx66rfSFgK6LNpIasVKkEgW8oyf0tmPLA== + +"@hotwired/stimulus-webpack-helpers@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@hotwired/stimulus-webpack-helpers/-/stimulus-webpack-helpers-1.0.1.tgz#4cd74487adeca576c9865ac2b9fe5cb20cef16dd" + integrity sha512-wa/zupVG0eWxRYJjC1IiPBdt3Lruv0RqGN+/DTMmUWUyMAEB27KXmVY6a8YpUVTM7QwVuaLNGW4EqDgrS2upXQ== + +"@hotwired/stimulus@^3.0.0": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.2.1.tgz#e3de23623b0c52c247aba4cd5d530d257008676b" + integrity sha512-HGlzDcf9vv/EQrMJ5ZG6VWNs8Z/xMN+1o2OhV1gKiSG6CqZt5MCBB1gRg5ILiN3U0jEAxuDTNPRfBcnZBDmupQ== + +"@humanwhocodes/config-array@^0.11.8": + version "0.11.8" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" + integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@jest/schemas@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.3.tgz#39cf1b8469afc40b6f5a2baaa146e332c4151788" + integrity sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg== + dependencies: + "@sinclair/typebox" "^0.25.16" + +"@jest/types@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.4.3.tgz#9069145f4ef09adf10cec1b2901b2d390031431f" + integrity sha512-bPYfw8V65v17m2Od1cv44FH+SiKW7w2Xu7trhcdTLUmSv85rfKsP+qXSjO4KGJr4dtPSzl/gvslZBXctf1qGEA== + dependencies: + "@jest/schemas" "^29.4.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" + integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" + integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.17" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" + integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" + integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@nuxt/friendly-errors-webpack-plugin@^2.5.1": + version "2.5.2" + resolved "https://registry.yarnpkg.com/@nuxt/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-2.5.2.tgz#982a43ee2da61611f7396439e57038392d3944d5" + integrity sha512-LLc+90lnxVbpKkMqk5z1EWpXoODhc6gRkqqXJCInJwF5xabHAE7biFvbULfvTRmtaTzAaP8IV4HQDLUgeAUTTw== + dependencies: + chalk "^2.3.2" + consola "^2.6.0" + error-stack-parser "^2.0.0" + string-width "^4.2.3" + +"@orchidjs/sifter@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@orchidjs/sifter/-/sifter-1.0.3.tgz#43f42519472282eb632d0a1589184f044d64129b" + integrity sha512-zCZbwKegHytfsPm8Amcfh7v/4vHqTAaOu6xFswBYcn8nznBOuseu6COB2ON7ez0tFV0mKL0nRNnCiZZA+lU9/g== + dependencies: + "@orchidjs/unicode-variants" "^1.0.4" + +"@orchidjs/unicode-variants@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@orchidjs/unicode-variants/-/unicode-variants-1.0.4.tgz#6d2f812e3b19545bba2d81caffff1204de9a6a58" + integrity sha512-NvVBRnZNE+dugiXERFsET1JlKZfM5lJDEpSMilKW4bToYJ7pxf0Zne78xyXB2ny2c2aHfJ6WLnz1AaTNHAmQeQ== + +"@popperjs/core@^2.11.6": + version "2.11.6" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" + integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== + +"@sinclair/typebox@^0.25.16": + version "0.25.24" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718" + integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ== + +"@symfony/stimulus-bridge@^3.2.0": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@symfony/stimulus-bridge/-/stimulus-bridge-3.2.1.tgz#b9c261ad72830fd17898cf27c97862d1cc15b46a" + integrity sha512-eawmVu+tLVkiTz7ewkcsxFvaSZKxFWXmdWxIsxr2jIfQ64PSJg7PIcd7GsPMDxX8sLwg3LGxXfJv5bASL1es+A== + dependencies: + "@hotwired/stimulus-webpack-helpers" "^1.0.1" + "@types/webpack-env" "^1.16.4" + acorn "^8.0.5" + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +"@symfony/ux-autocomplete@file:vendor/symfony/ux-autocomplete/assets": + version "1.0.0" + +"@symfony/webpack-encore@^4.0.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@symfony/webpack-encore/-/webpack-encore-4.2.0.tgz#1eec87e3fca9a410563511eb557f70461c10cce5" + integrity sha512-m0ZGm7vZpmc9pVKE7YBppS1tb9bK8r0qzOvRI2uCK7UfXtzfV3VgXr0VdTAlfmC72vvLKI+s9YJpiesOFbR6Aw== + dependencies: + "@nuxt/friendly-errors-webpack-plugin" "^2.5.1" + assets-webpack-plugin "7.0.*" + babel-loader "^8.2.5" + chalk "^4.0.0" + clean-webpack-plugin "^4.0.0" + css-loader "^6.7.0" + css-minimizer-webpack-plugin "^4.0.0" + fast-levenshtein "^3.0.0" + mini-css-extract-plugin "^2.6.0" + pkg-up "^3.1.0" + pretty-error "^4.0.0" + resolve-url-loader "^5.0.0" + semver "^7.3.2" + style-loader "^3.3.0" + sync-rpc "^1.3.6" + tapable "^2.2.1" + terser-webpack-plugin "^5.3.0" + tmp "^0.2.1" + webpack-dev-server "^4.8.0" + yargs-parser "^21.0.0" + +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== + +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.9": + version "3.5.10" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.10.tgz#0f6aadfe00ea414edc86f5d106357cda9701e275" + integrity sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw== + dependencies: + "@types/node" "*" + +"@types/connect-history-api-fallback@^1.3.5": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae" + integrity sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.21.1" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.21.1.tgz#110b441a210d53ab47795124dbc3e9bb993d1e7c" + integrity sha512-rc9K8ZpVjNcLs8Fp0dkozd5Pt2Apk1glO4Vgz8ix1u6yFByxfqo5Yavpy65o+93TAe24jr7v+eSBtFLvOQtCRQ== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" + integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== + +"@types/estree@^0.0.51": + version "0.0.51" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": + version "4.17.33" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543" + integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@*", "@types/express@^4.17.13": + version "4.17.17" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" + integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/glob@^7.1.1": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" + integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + +"@types/http-proxy@^1.17.8": + version "1.17.10" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.10.tgz#e576c8e4a0cc5c6a138819025a88e167ebb38d6c" + integrity sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/mime@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" + integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== + +"@types/minimatch@*": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" + integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== + +"@types/node@*": + version "18.14.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.2.tgz#c076ed1d7b6095078ad3cf21dfeea951842778b1" + integrity sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA== + +"@types/qs@*": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/retry@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + +"@types/serve-index@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278" + integrity sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg== + dependencies: + "@types/express" "*" + +"@types/serve-static@*", "@types/serve-static@^1.13.10": + version "1.15.1" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.1.tgz#86b1753f0be4f9a1bee68d459fcda5be4ea52b5d" + integrity sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ== + dependencies: + "@types/mime" "*" + "@types/node" "*" + +"@types/sockjs@^0.3.33": + version "0.3.33" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f" + integrity sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw== + dependencies: + "@types/node" "*" + +"@types/webpack-env@^1.16.4": + version "1.18.0" + resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.18.0.tgz#ed6ecaa8e5ed5dfe8b2b3d00181702c9925f13fb" + integrity sha512-56/MAlX5WMsPVbOg7tAxnYvNYMMWr/QJiIp6BxVSW3JJXUVzzOn64qW8TzQyMSqSUFM2+PVI4aUHcHOzIz/1tg== + +"@types/ws@^8.5.1": + version "8.5.4" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5" + integrity sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg== + dependencies: + "@types/node" "*" + +"@types/yargs-parser@*": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + +"@types/yargs@^17.0.8": + version "17.0.22" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.22.tgz#7dd37697691b5f17d020f3c63e7a45971ff71e9a" + integrity sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g== + dependencies: + "@types/yargs-parser" "*" + +"@webassemblyjs/ast@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" + integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + +"@webassemblyjs/floating-point-hex-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" + integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== + +"@webassemblyjs/helper-api-error@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" + integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== + +"@webassemblyjs/helper-buffer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" + integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== + +"@webassemblyjs/helper-numbers@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" + integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" + integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== + +"@webassemblyjs/helper-wasm-section@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" + integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + +"@webassemblyjs/ieee754@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" + integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" + integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" + integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== + +"@webassemblyjs/wasm-edit@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" + integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-wasm-section" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-opt" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + "@webassemblyjs/wast-printer" "1.11.1" + +"@webassemblyjs/wasm-gen@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" + integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wasm-opt@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" + integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + +"@webassemblyjs/wasm-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" + integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wast-printer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" + integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.2.0.tgz#7b20ce1c12533912c3b217ea68262365fa29a6f5" + integrity sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg== + +"@webpack-cli/info@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.5.0.tgz#6c78c13c5874852d6e2dd17f08a41f3fe4c261b1" + integrity sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ== + dependencies: + envinfo "^7.7.3" + +"@webpack-cli/serve@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1" + integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-assertions@^1.7.6: + version "1.8.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" + integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.0.5, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + +adjust-sourcemap-loader@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99" + integrity sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A== + dependencies: + loader-utils "^2.0.0" + regex-parser "^2.2.11" + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.8.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-flatten@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng== + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q== + +assets-webpack-plugin@7.0.*: + version "7.0.0" + resolved "https://registry.yarnpkg.com/assets-webpack-plugin/-/assets-webpack-plugin-7.0.0.tgz#c61ed7466f35ff7a4d90d7070948736f471b8804" + integrity sha512-DMZ9r6HFxynWeONRMhSOFTvTrmit5dovdoUKdJgCG03M6CC7XiwNImPH+Ad1jaVrQ2n59e05lBhte52xPt4MSA== + dependencies: + camelcase "^6.0.0" + escape-string-regexp "^4.0.0" + lodash "^4.17.20" + +babel-loader@^8.2.5: + version "8.3.0" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8" + integrity sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q== + dependencies: + find-cache-dir "^3.3.1" + loader-utils "^2.0.0" + make-dir "^3.1.0" + schema-utils "^2.6.5" + +babel-plugin-polyfill-corejs2@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz#5d1bd3836d0a19e1b84bbf2d9640ccb6f951c122" + integrity sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q== + dependencies: + "@babel/compat-data" "^7.17.7" + "@babel/helper-define-polyfill-provider" "^0.3.3" + semver "^6.1.1" + +babel-plugin-polyfill-corejs3@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz#56ad88237137eade485a71b52f72dbed57c6230a" + integrity sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.3" + core-js-compat "^3.25.1" + +babel-plugin-polyfill-regenerator@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz#390f91c38d90473592ed43351e801a9d3e0fd747" + integrity sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.3" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour-service@^1.0.11: + version "1.1.0" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.1.0.tgz#424170268d68af26ff83a5c640b95def01803a13" + integrity sha512-LVRinRB3k1/K0XzZ2p58COnWvkQknIY6sf0zF2rpErvcJXpMBttEPQSxK+HEXSS9VmpZlDoDnQWv8ftJT20B0Q== + dependencies: + array-flatten "^2.1.2" + dns-equal "^1.0.0" + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + +bootstrap-icons@^1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/bootstrap-icons/-/bootstrap-icons-1.10.3.tgz#c587b078ca6743bef4653fe90434b4aebfba53b2" + integrity sha512-7Qvj0j0idEm/DdX9Q0CpxAnJYqBCFCiUI6qzSPYfERMcokVuV9Mdm/AJiVZI8+Gawe4h/l6zFcOzvV7oXCZArw== + +bootstrap@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.2.3.tgz#54739f4414de121b9785c5da3c87b37ff008322b" + integrity sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.21.3, browserslist@^4.21.4, browserslist@^4.21.5: + version "4.21.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" + integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== + dependencies: + caniuse-lite "^1.0.30001449" + electron-to-chromium "^1.4.284" + node-releases "^2.0.8" + update-browserslist-db "^1.0.10" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001449: + version "1.0.30001458" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz#871e35866b4654a7d25eccca86864f411825540c" + integrity sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w== + +chalk@^2.0.0, chalk@^2.3.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +ci-info@^3.2.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" + integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== + +clean-webpack-plugin@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz#72947d4403d452f38ed61a9ff0ada8122aacd729" + integrity sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w== + dependencies: + del "^4.1.1" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colord@^2.9.1: + version "2.9.3" + resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" + integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== + +colorette@^2.0.10, colorette@^2.0.14: + version "2.0.19" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^7.0.0, commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + +consola@^2.6.0: + version "2.15.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" + integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@^1.7.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +core-js-compat@^3.25.1: + version "3.29.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.29.0.tgz#1b8d9eb4191ab112022e7f6364b99b65ea52f528" + integrity sha512-ScMn3uZNAFhK2DGoEfErguoiAHhV2Ju+oJo/jK08p7B3f3UhocUrCCkTvnZaiS+edl5nlIoiBXKcwMc6elv4KQ== + dependencies: + browserslist "^4.21.5" + +core-js@^3.23.0: + version "3.29.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.29.0.tgz#0273e142b67761058bcde5615c503c7406b572d6" + integrity sha512-VG23vuEisJNkGl6XQmFJd3rEG/so/CNatqeE+7uZAwTSwFeB/qaO0be8xZYUNWprJ/GIwL8aMt9cj1kvbpTZhg== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +css-declaration-sorter@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz#be5e1d71b7a992433fb1c542c7a1b835e45682ec" + integrity sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w== + +css-loader@^6.7.0: + version "6.7.3" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.3.tgz#1e8799f3ccc5874fdd55461af51137fcc5befbcd" + integrity sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ== + dependencies: + icss-utils "^5.1.0" + postcss "^8.4.19" + postcss-modules-extract-imports "^3.0.0" + postcss-modules-local-by-default "^4.0.0" + postcss-modules-scope "^3.0.0" + postcss-modules-values "^4.0.0" + postcss-value-parser "^4.2.0" + semver "^7.3.8" + +css-minimizer-webpack-plugin@^4.0.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-4.2.2.tgz#79f6199eb5adf1ff7ba57f105e3752d15211eb35" + integrity sha512-s3Of/4jKfw1Hj9CxEO1E5oXhQAxlayuHO2y/ML+C6I9sQ7FdzfEV6QgMLN3vI+qFsjJGIAFLKtQK7t8BOXAIyA== + dependencies: + cssnano "^5.1.8" + jest-worker "^29.1.2" + postcss "^8.4.17" + schema-utils "^4.0.0" + serialize-javascript "^6.0.0" + source-map "^0.6.1" + +css-select@^4.1.3: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== + dependencies: + boolbase "^1.0.0" + css-what "^6.0.1" + domhandler "^4.3.1" + domutils "^2.8.0" + nth-check "^2.0.1" + +css-tree@^1.1.2, css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-what@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssnano-preset-default@^5.2.14: + version "5.2.14" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz#309def4f7b7e16d71ab2438052093330d9ab45d8" + integrity sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A== + dependencies: + css-declaration-sorter "^6.3.1" + cssnano-utils "^3.1.0" + postcss-calc "^8.2.3" + postcss-colormin "^5.3.1" + postcss-convert-values "^5.1.3" + postcss-discard-comments "^5.1.2" + postcss-discard-duplicates "^5.1.0" + postcss-discard-empty "^5.1.1" + postcss-discard-overridden "^5.1.0" + postcss-merge-longhand "^5.1.7" + postcss-merge-rules "^5.1.4" + postcss-minify-font-values "^5.1.0" + postcss-minify-gradients "^5.1.1" + postcss-minify-params "^5.1.4" + postcss-minify-selectors "^5.2.1" + postcss-normalize-charset "^5.1.0" + postcss-normalize-display-values "^5.1.0" + postcss-normalize-positions "^5.1.1" + postcss-normalize-repeat-style "^5.1.1" + postcss-normalize-string "^5.1.0" + postcss-normalize-timing-functions "^5.1.0" + postcss-normalize-unicode "^5.1.1" + postcss-normalize-url "^5.1.0" + postcss-normalize-whitespace "^5.1.1" + postcss-ordered-values "^5.1.3" + postcss-reduce-initial "^5.1.2" + postcss-reduce-transforms "^5.1.0" + postcss-svgo "^5.1.0" + postcss-unique-selectors "^5.1.1" + +cssnano-utils@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz#95684d08c91511edfc70d2636338ca37ef3a6861" + integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== + +cssnano@^5.1.8: + version "5.1.15" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.15.tgz#ded66b5480d5127fcb44dac12ea5a983755136bf" + integrity sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw== + dependencies: + cssnano-preset-default "^5.2.14" + lilconfig "^2.0.3" + yaml "^1.10.2" + +csso@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.1.0, debug@^4.1.1, debug@^4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +default-gateway@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" + integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== + dependencies: + execa "^5.0.0" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +del@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" + integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== + dependencies: + "@types/glob" "^7.1.1" + globby "^6.1.0" + is-path-cwd "^2.0.0" + is-path-in-cwd "^2.0.0" + p-map "^2.0.0" + pify "^4.0.1" + rimraf "^2.6.3" + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + integrity sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg== + +dns-packet@^5.2.2: + version "5.4.0" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.4.0.tgz#1f88477cf9f27e78a213fb6d118ae38e759a879b" + integrity sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-converter@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" + integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== + dependencies: + utila "~0.4" + +dom-serializer@^1.0.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +dom7@^4.0.4: + version "4.0.6" + resolved "https://registry.yarnpkg.com/dom7/-/dom7-4.0.6.tgz#091a51621d7a19ce0fb86045cafb3c10035e97ed" + integrity sha512-emjdpPLhpNubapLFdjNL9tP06Sr+GZkrIHEXLWvOGsytACUrkbeIdjO5g77m00BrHTznnlcNqgmn7pCN192TBA== + dependencies: + ssr-window "^4.0.0" + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + +domutils@^2.5.2, domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.4.284: + version "1.4.314" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.314.tgz#33e4ad7a2ca2ddbe2e113874cc0c0e2a00cb46bf" + integrity sha512-+3RmNVx9hZLlc0gW//4yep0K5SYKmIvB5DXg1Yg6varsuAHlHwTeqeygfS8DWwLCsNOWrgj+p9qgM5WYjw1lXQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +enhanced-resolve@^5.10.0: + version "5.12.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" + integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +envinfo@^7.7.3: + version "7.8.1" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" + integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== + +error-stack-parser@^2.0.0: + version "2.1.4" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" + integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== + dependencies: + stackframe "^1.3.4" + +es-module-lexer@^0.9.0: + version "0.9.3" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" + integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint@^8.33.0: + version "8.35.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.35.0.tgz#fffad7c7e326bae606f0e8f436a6158566d42323" + integrity sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw== + dependencies: + "@eslint/eslintrc" "^2.0.0" + "@eslint/js" "8.35.0" + "@humanwhocodes/config-array" "^0.11.8" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.4.0" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-sdsl "^4.1.4" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + +espree@^9.4.0: + version "9.4.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.1.tgz#51d6092615567a2c2cff7833445e37c28c0065bd" + integrity sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg== + dependencies: + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.3.0" + +esquery@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.2.tgz#c6d3fee05dd665808e2ad870631f221f5617b1d1" + integrity sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +express@^4.17.3: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fast-levenshtein@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz#37b899ae47e1090e40e3fd2318e4d5f0142ca912" + integrity sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ== + dependencies: + fastest-levenshtein "^1.0.7" + +fastest-levenshtein@^1.0.12, fastest-levenshtein@^1.0.7: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +fastq@^1.6.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + dependencies: + reusify "^1.0.4" + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-cache-dir@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatpickr@^4.6.13: + version "4.6.13" + resolved "https://registry.yarnpkg.com/flatpickr/-/flatpickr-4.6.13.tgz#8a029548187fd6e0d670908471e43abe9ad18d94" + integrity sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw== + +flatted@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + +follow-redirects@^1.0.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-monkey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" + integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-intrinsic@^1.0.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" + integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-port@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc" + integrity sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg== + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^7.0.3, glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.19.0: + version "13.20.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" + integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== + dependencies: + type-fest "^0.20.2" + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + integrity sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw== + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + integrity sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw== + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-entities@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46" + integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA== + +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + +http-proxy-middleware@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +icss-utils@^5.0.0, icss-utils@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + +ignore@^5.2.0: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + +immutable@^4.0.0: + version "4.2.4" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.4.tgz#83260d50889526b4b531a5e293709a77f7c55a2a" + integrity sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" + integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" + integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== + dependencies: + has "^1.0.3" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-cwd@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" + integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== + +is-path-in-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" + integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== + dependencies: + is-path-inside "^2.1.0" + +is-path-inside@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" + integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== + dependencies: + path-is-inside "^1.0.2" + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jest-util@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.4.3.tgz#851a148e23fc2b633c55f6dad2e45d7f4579f496" + integrity sha512-ToSGORAz4SSSoqxDSylWX8JzkOQR7zoBtNRsA7e+1WUX5F8jrOwaNpuh1YfJHJKDHXLHmObv5eOjejUd+/Ws+Q== + dependencies: + "@jest/types" "^29.4.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest-worker@^29.1.2: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.4.3.tgz#9a4023e1ea1d306034237c7133d7da4240e8934e" + integrity sha512-GLHN/GTAAMEy5BFdvpUfzr9Dr80zQqBrh0fz1mtRMe05hqP45+HfQltu7oTBfduD0UeZs09d+maFtFYAXFWvAA== + dependencies: + "@types/node" "*" + jest-util "^29.4.3" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +js-sdsl@^4.1.4: + version "4.3.0" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" + integrity sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^2.1.2, json5@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +klona@^2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" + integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lilconfig@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4" + integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +loader-utils@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" + integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== + +lodash@^4.17.20, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-dir@^3.0.2, make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memfs@^3.4.3: + version "3.4.13" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.13.tgz#248a8bd239b3c240175cd5ec548de5227fc4f345" + integrity sha512-omTM41g3Skpvx5dSYeZIbXKcXoAVc/AoMNwn9TKx++L/gaen/+4TTttmu8ZSch5vfVJ8uJvGbroTsIlslRg6lg== + dependencies: + fs-monkey "^1.0.3" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.2: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mini-css-extract-plugin@^2.6.0: + version "2.7.2" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.2.tgz#e049d3ea7d3e4e773aad585c6cb329ce0c7b72d7" + integrity sha512-EdlUizq13o0Pd+uCp+WO/JpkLvHRVGt97RqfeGhXqAcorYo1ypJSpkV+WDT0vY/kmh/p7wRdJNJtuyK540PXDw== + dependencies: + schema-utils "^4.0.0" + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + +nanoid@^3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" + integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-notifier@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-9.0.1.tgz#cea837f4c5e733936c7b9005e6545cea825d1af4" + integrity sha512-fPNFIp2hF/Dq7qLDzSg4vZ0J4e9v60gJR+Qx7RbjbWqzPDdEqeVpEx5CFeDAELIl+A/woaaNn1fQ5nEVerMxJg== + dependencies: + growly "^1.3.0" + is-wsl "^2.2.0" + semver "^7.3.2" + shellwords "^0.1.1" + uuid "^8.3.0" + which "^2.0.2" + +node-releases@^2.0.8: + version "2.0.10" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" + integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.9.0: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^8.0.9: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +p-limit@^2.0.0, p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-map@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + +p-retry@^4.5.0: + version "4.6.2" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" + integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== + dependencies: + "@types/retry" "0.12.0" + retry "^0.13.1" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw== + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== + +pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pkg-up@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" + integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== + dependencies: + find-up "^3.0.0" + +postcss-calc@^8.2.3: + version "8.2.4" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5" + integrity sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q== + dependencies: + postcss-selector-parser "^6.0.9" + postcss-value-parser "^4.2.0" + +postcss-colormin@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.1.tgz#86c27c26ed6ba00d96c79e08f3ffb418d1d1988f" + integrity sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ== + dependencies: + browserslist "^4.21.4" + caniuse-api "^3.0.0" + colord "^2.9.1" + postcss-value-parser "^4.2.0" + +postcss-convert-values@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz#04998bb9ba6b65aa31035d669a6af342c5f9d393" + integrity sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA== + dependencies: + browserslist "^4.21.4" + postcss-value-parser "^4.2.0" + +postcss-discard-comments@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz#8df5e81d2925af2780075840c1526f0660e53696" + integrity sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ== + +postcss-discard-duplicates@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz#9eb4fe8456706a4eebd6d3b7b777d07bad03e848" + integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw== + +postcss-discard-empty@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz#e57762343ff7f503fe53fca553d18d7f0c369c6c" + integrity sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A== + +postcss-discard-overridden@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz#7e8c5b53325747e9d90131bb88635282fb4a276e" + integrity sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw== + +postcss-merge-longhand@^5.1.7: + version "5.1.7" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz#24a1bdf402d9ef0e70f568f39bdc0344d568fb16" + integrity sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ== + dependencies: + postcss-value-parser "^4.2.0" + stylehacks "^5.1.1" + +postcss-merge-rules@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz#2f26fa5cacb75b1402e213789f6766ae5e40313c" + integrity sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g== + dependencies: + browserslist "^4.21.4" + caniuse-api "^3.0.0" + cssnano-utils "^3.1.0" + postcss-selector-parser "^6.0.5" + +postcss-minify-font-values@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz#f1df0014a726083d260d3bd85d7385fb89d1f01b" + integrity sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-minify-gradients@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz#f1fe1b4f498134a5068240c2f25d46fcd236ba2c" + integrity sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw== + dependencies: + colord "^2.9.1" + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-minify-params@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz#c06a6c787128b3208b38c9364cfc40c8aa5d7352" + integrity sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw== + dependencies: + browserslist "^4.21.4" + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-minify-selectors@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz#d4e7e6b46147b8117ea9325a915a801d5fe656c6" + integrity sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg== + dependencies: + postcss-selector-parser "^6.0.5" + +postcss-modules-extract-imports@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" + integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== + +postcss-modules-local-by-default@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" + integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" + integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + +postcss-normalize-charset@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz#9302de0b29094b52c259e9b2cf8dc0879879f0ed" + integrity sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg== + +postcss-normalize-display-values@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz#72abbae58081960e9edd7200fcf21ab8325c3da8" + integrity sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-positions@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz#ef97279d894087b59325b45c47f1e863daefbb92" + integrity sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-repeat-style@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz#e9eb96805204f4766df66fd09ed2e13545420fb2" + integrity sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-string@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz#411961169e07308c82c1f8c55f3e8a337757e228" + integrity sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-timing-functions@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz#d5614410f8f0b2388e9f240aa6011ba6f52dafbb" + integrity sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-unicode@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz#f67297fca3fea7f17e0d2caa40769afc487aa030" + integrity sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA== + dependencies: + browserslist "^4.21.4" + postcss-value-parser "^4.2.0" + +postcss-normalize-url@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz#ed9d88ca82e21abef99f743457d3729a042adcdc" + integrity sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew== + dependencies: + normalize-url "^6.0.1" + postcss-value-parser "^4.2.0" + +postcss-normalize-whitespace@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz#08a1a0d1ffa17a7cc6efe1e6c9da969cc4493cfa" + integrity sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-ordered-values@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz#b6fd2bd10f937b23d86bc829c69e7732ce76ea38" + integrity sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ== + dependencies: + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-reduce-initial@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz#798cd77b3e033eae7105c18c9d371d989e1382d6" + integrity sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg== + dependencies: + browserslist "^4.21.4" + caniuse-api "^3.0.0" + +postcss-reduce-transforms@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz#333b70e7758b802f3dd0ddfe98bb1ccfef96b6e9" + integrity sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: + version "6.0.11" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" + integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-svgo@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d" + integrity sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA== + dependencies: + postcss-value-parser "^4.2.0" + svgo "^2.7.0" + +postcss-unique-selectors@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz#a9f273d1eacd09e9aa6088f4b0507b18b1b541b6" + integrity sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA== + dependencies: + postcss-selector-parser "^6.0.5" + +postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^8.2.14, postcss@^8.4.17, postcss@^8.4.19: + version "8.4.21" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" + integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +pretty-error@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" + integrity sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw== + dependencies: + lodash "^4.17.20" + renderkid "^3.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +punycode@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readable-stream@^2.0.1: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6: + version "3.6.1" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.1.tgz#f9f9b5f536920253b3d26e7660e7da4ccff9bb62" + integrity sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" + integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg== + dependencies: + resolve "^1.9.0" + +regenerate-unicode-properties@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" + integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.9: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + +regenerator-transform@^0.15.1: + version "0.15.1" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" + integrity sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg== + dependencies: + "@babel/runtime" "^7.8.4" + +regex-parser@^2.2.11: + version "2.2.11" + resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58" + integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== + +regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +regexpu-core@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.1.tgz#66900860f88def39a5cb79ebd9490e84f17bcdfb" + integrity sha512-nCOzW2V/X15XpLsK2rlgdwrysrBq+AauCn+omItIz4R1pIcmeot5zvjdmOBRLzEH/CkC6IxMJVmxDe3QcMuNVQ== + dependencies: + "@babel/regjsgen" "^0.8.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^10.1.0" + regjsparser "^0.9.1" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +regjsparser@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" + integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== + dependencies: + jsesc "~0.5.0" + +renderkid@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" + integrity sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg== + dependencies: + css-select "^4.1.3" + dom-converter "^0.2.0" + htmlparser2 "^6.1.0" + lodash "^4.17.21" + strip-ansi "^6.0.1" + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve-url-loader@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz#ee3142fb1f1e0d9db9524d539cfa166e9314f795" + integrity sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg== + dependencies: + adjust-sourcemap-loader "^4.0.0" + convert-source-map "^1.7.0" + loader-utils "^2.0.0" + postcss "^8.2.14" + source-map "0.6.1" + +resolve@^1.14.2, resolve@^1.9.0: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sass-loader@^13.0.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-13.2.0.tgz#80195050f58c9aac63b792fa52acb6f5e0f6bdc3" + integrity sha512-JWEp48djQA4nbZxmgC02/Wh0eroSUutulROUusYJO9P9zltRbNN80JCBHqRGzjd4cmZCa/r88xgfkjGD0TXsHg== + dependencies: + klona "^2.0.4" + neo-async "^2.6.2" + +sass@^1.56.1: + version "1.58.3" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.58.3.tgz#2348cc052061ba4f00243a208b09c40e031f270d" + integrity sha512-Q7RaEtYf6BflYrQ+buPudKR26/lH+10EmO9bBqbmPh/KeLqv8bjpTNqxe71ocONqXq+jYiCbpPUmQMS+JJPk4A== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + +schema-utils@^2.6.5: + version "2.7.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" + integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== + dependencies: + "@types/json-schema" "^7.0.5" + ajv "^6.12.4" + ajv-keywords "^3.5.2" + +schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7" + integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.8.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.0.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.1.1.tgz#18a7613d714c0cd3385c48af0075abf3f266af61" + integrity sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ== + dependencies: + node-forge "^1" + +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.3.2, semver@^7.3.8: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" + integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +sockjs@^0.3.24: + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +ssr-window@^4.0.0, ssr-window@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/ssr-window/-/ssr-window-4.0.2.tgz#dc6b3ee37be86ac0e3ddc60030f7b3bc9b8553be" + integrity sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ== + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + +stackframe@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" + integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +stimulus-carousel@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/stimulus-carousel/-/stimulus-carousel-5.0.1.tgz#87306f7619ea64a8c36258f6f90f2989089c848d" + integrity sha512-j37fjoUc/4hkroxuEK6OuhYMd7d9sZN/5pKB5Y+PJzM9Wy5KvAfsURW4c7zMo58BQAlVrrz45XJrHc/XleEU3Q== + dependencies: + swiper "^8.4.5" + +stimulus-password-visibility@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/stimulus-password-visibility/-/stimulus-password-visibility-2.1.1.tgz#b366223c7650704507a38a3e5d126e8474abad3e" + integrity sha512-FnscC06Ux7mgfJo9s7OceOQ59yU1Tm8sWByG89hnMPcWvYSX/1yx3wFxNOgKhtxaDPdvtb+ERgmv6F9JvPJc7w== + +string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +style-loader@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" + integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ== + +stylehacks@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.1.tgz#7934a34eb59d7152149fa69d6e9e56f2fc34bcc9" + integrity sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw== + dependencies: + browserslist "^4.21.4" + postcss-selector-parser "^6.0.4" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svgo@^2.7.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^4.1.3" + css-tree "^1.1.3" + csso "^4.2.0" + picocolors "^1.0.0" + stable "^0.1.8" + +swiper@^8.4.5: + version "8.4.7" + resolved "https://registry.yarnpkg.com/swiper/-/swiper-8.4.7.tgz#0301d385c3efc8efe8b66a64187edcb30e3067ee" + integrity sha512-VwO/KU3i9IV2Sf+W2NqyzwWob4yX9Qdedq6vBtS0rFqJ6Fa5iLUJwxQkuD4I38w0WDJwmFl8ojkdcRFPHWD+2g== + dependencies: + dom7 "^4.0.4" + ssr-window "^4.0.2" + +sync-rpc@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/sync-rpc/-/sync-rpc-1.3.6.tgz#b2e8b2550a12ccbc71df8644810529deb68665a7" + integrity sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw== + dependencies: + get-port "^3.1.0" + +tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.3.0: + version "5.3.6" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz#5590aec31aa3c6f771ce1b1acca60639eab3195c" + integrity sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ== + dependencies: + "@jridgewell/trace-mapping" "^0.3.14" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.0" + terser "^5.14.1" + +terser@^5.14.1: + version "5.16.5" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.5.tgz#1c285ca0655f467f92af1bbab46ab72d1cb08e5a" + integrity sha512-qcwfg4+RZa3YvlFh0qjifnzBHjKGNbtDo9yivMqMFDy9Q6FSaQWSB/j1xKhsoUFJIqDOM3TsN6D5xbrMrFcHbg== + dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +tmp@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tom-select@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.2.2.tgz#8e5f9296e6d80254feccb57f0986bd6c44d126e2" + integrity sha512-igGah1yY6yhrnN2h/Ky8I5muw/nE/YQxIsEZoYu5qaA4bsRibvKto3s8QZZosKpOd0uO8fNYhRfAwgHB4IAYew== + dependencies: + "@orchidjs/sifter" "^1.0.3" + "@orchidjs/unicode-variants" "^1.0.4" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" + integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" + integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utila@~0.4: + version "0.4.0" + resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.0, uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webpack-cli@^4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.10.0.tgz#37c1d69c8d85214c5a65e589378f53aec64dab31" + integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^1.2.0" + "@webpack-cli/info" "^1.5.0" + "@webpack-cli/serve" "^1.7.0" + colorette "^2.0.14" + commander "^7.0.0" + cross-spawn "^7.0.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^2.2.0" + rechoir "^0.7.0" + webpack-merge "^5.7.3" + +webpack-dev-middleware@^5.3.1: + version "5.3.3" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" + integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== + dependencies: + colorette "^2.0.10" + memfs "^3.4.3" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@^4.8.0: + version "4.11.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz#ae07f0d71ca0438cf88446f09029b92ce81380b5" + integrity sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw== + dependencies: + "@types/bonjour" "^3.5.9" + "@types/connect-history-api-fallback" "^1.3.5" + "@types/express" "^4.17.13" + "@types/serve-index" "^1.9.1" + "@types/serve-static" "^1.13.10" + "@types/sockjs" "^0.3.33" + "@types/ws" "^8.5.1" + ansi-html-community "^0.0.8" + bonjour-service "^1.0.11" + chokidar "^3.5.3" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^2.0.0" + default-gateway "^6.0.3" + express "^4.17.3" + graceful-fs "^4.2.6" + html-entities "^2.3.2" + http-proxy-middleware "^2.0.3" + ipaddr.js "^2.0.1" + open "^8.0.9" + p-retry "^4.5.0" + rimraf "^3.0.2" + schema-utils "^4.0.0" + selfsigned "^2.1.1" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^5.3.1" + ws "^8.4.2" + +webpack-merge@^5.7.3: + version "5.8.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61" + integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q== + dependencies: + clone-deep "^4.0.1" + wildcard "^2.0.0" + +webpack-notifier@^1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/webpack-notifier/-/webpack-notifier-1.15.0.tgz#72644a1a4ec96b3528704d28f79da5e70048e8ee" + integrity sha512-N2V8UMgRB5komdXQRavBsRpw0hPhJq2/SWNOGuhrXpIgRhcMexzkGQysUyGStHLV5hkUlgpRiF7IUXoBqyMmzQ== + dependencies: + node-notifier "^9.0.0" + strip-ansi "^6.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@^5.74.0: + version "5.75.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.75.0.tgz#1e440468647b2505860e94c9ff3e44d5b582c152" + integrity sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^0.0.51" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/wasm-edit" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + acorn "^8.7.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.10.0" + es-module-lexer "^0.9.0" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.3" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +which@^2.0.1, which@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" + integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@^8.4.2: + version "8.12.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.1.tgz#c51e583d79140b5e42e39be48c934131942d4a8f" + integrity sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yargs-parser@^21.0.0: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==