Compare commits
No commits in common. "develop" and "main" have entirely different histories.
739 changed files with 2013 additions and 118028 deletions
|
|
@ -1,24 +0,0 @@
|
||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
charset = utf-8
|
|
||||||
end_of_line = lf
|
|
||||||
indent_size = 4
|
|
||||||
indent_style = space
|
|
||||||
insert_final_newline = true
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
|
|
||||||
[*.md]
|
|
||||||
trim_trailing_whitespace = false
|
|
||||||
|
|
||||||
[*.{yml,yaml}]
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[docker-compose.yml]
|
|
||||||
indent_size = 4
|
|
||||||
|
|
||||||
[*.scss]
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.{tsx,ts,cjs}]
|
|
||||||
indent_size = 2
|
|
||||||
79
.env.example
79
.env.example
|
|
@ -1,79 +0,0 @@
|
||||||
APP_NAME=Laravel
|
|
||||||
APP_ENV=local
|
|
||||||
APP_KEY=
|
|
||||||
APP_DEBUG=true
|
|
||||||
APP_URL=http://localhost
|
|
||||||
|
|
||||||
APP_LOCALE=en
|
|
||||||
APP_FALLBACK_LOCALE=en
|
|
||||||
APP_FAKER_LOCALE=en_US
|
|
||||||
|
|
||||||
APP_MAINTENANCE_DRIVER=file
|
|
||||||
# APP_MAINTENANCE_STORE=database
|
|
||||||
|
|
||||||
# PHP_CLI_SERVER_WORKERS=4
|
|
||||||
|
|
||||||
BCRYPT_ROUNDS=12
|
|
||||||
|
|
||||||
LOG_CHANNEL=stack
|
|
||||||
LOG_STACK=single
|
|
||||||
LOG_DEPRECATIONS_CHANNEL=null
|
|
||||||
LOG_LEVEL=debug
|
|
||||||
|
|
||||||
DB_CONNECTION=sqlite
|
|
||||||
# DB_HOST=127.0.0.1
|
|
||||||
# DB_PORT=3306
|
|
||||||
# DB_DATABASE=laravel
|
|
||||||
# DB_USERNAME=root
|
|
||||||
# DB_PASSWORD=
|
|
||||||
|
|
||||||
SESSION_DRIVER=database
|
|
||||||
SESSION_LIFETIME=120
|
|
||||||
SESSION_ENCRYPT=false
|
|
||||||
SESSION_PATH=/
|
|
||||||
SESSION_DOMAIN=null
|
|
||||||
|
|
||||||
BROADCAST_CONNECTION=log
|
|
||||||
FILESYSTEM_DISK=local
|
|
||||||
QUEUE_CONNECTION=database
|
|
||||||
|
|
||||||
CACHE_STORE=database
|
|
||||||
# CACHE_PREFIX=
|
|
||||||
|
|
||||||
MEMCACHED_HOST=127.0.0.1
|
|
||||||
|
|
||||||
REDIS_CLIENT=phpredis
|
|
||||||
REDIS_HOST=127.0.0.1
|
|
||||||
REDIS_PASSWORD=null
|
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
||||||
MAIL_MAILER=smtp
|
|
||||||
MAIL_SCHEME=null
|
|
||||||
MAIL_HOST=127.0.0.1
|
|
||||||
MAIL_PORT=2525
|
|
||||||
MAIL_USERNAME=null
|
|
||||||
MAIL_PASSWORD=null
|
|
||||||
MAIL_FROM_ADDRESS="hello@example.com"
|
|
||||||
MAIL_FROM_NAME="${APP_NAME}"
|
|
||||||
|
|
||||||
SAML2_SP_CERT_x509=
|
|
||||||
SAML2_SP_CERT_PRIVATEKEY=
|
|
||||||
SAML2_CONTACT_TECHNICAL_EMAIL=
|
|
||||||
SAML2_CONTACT_SUPPORT_EMAIL=
|
|
||||||
SAML2_DEFAULT_TENANT_UUID=
|
|
||||||
|
|
||||||
AWS_ACCESS_KEY_ID=minioadmin
|
|
||||||
AWS_SECRET_ACCESS_KEY=minioadmin
|
|
||||||
AWS_DEFAULT_REGION=us-east-1
|
|
||||||
AWS_BUCKET=ovoteam
|
|
||||||
AWS_ENDPOINT=http://127.0.0.1:9000
|
|
||||||
AWS_URL=http://127.0.0.1:9000/ovoteam
|
|
||||||
AWS_USE_PATH_STYLE_ENDPOINT=true
|
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
|
||||||
|
|
||||||
SCOUT_DRIVER=meilisearch
|
|
||||||
MEILISEARCH_HOST=
|
|
||||||
MEILISEARCH_KEY=
|
|
||||||
|
|
||||||
STORAGE_SERVICE_PREFIX=media
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
node_modules
|
|
||||||
public
|
|
||||||
vendor
|
|
||||||
resources/ts/components/ui/
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
sourceType: "module",
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true,
|
|
||||||
},
|
|
||||||
project: "./tsconfig.json",
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
version: "detect",
|
|
||||||
},
|
|
||||||
"import/resolver": {
|
|
||||||
typescript: {
|
|
||||||
project: "./tsconfig.json",
|
|
||||||
},
|
|
||||||
alias: {
|
|
||||||
map: [
|
|
||||||
["@", "./resources/ts"],
|
|
||||||
["&", "./resources/assets"],
|
|
||||||
],
|
|
||||||
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es2021: true,
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
ignorePatterns: ["resources/ts/components/ui/**"],
|
|
||||||
extends: [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:react-hooks/recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:prettier/recommended",
|
|
||||||
],
|
|
||||||
plugins: ["react", "react-hooks", "@typescript-eslint", "import", "prettier"],
|
|
||||||
rules: {
|
|
||||||
"no-undef": "off",
|
|
||||||
"react/display-name": "off",
|
|
||||||
"react/jsx-key": "off",
|
|
||||||
"react/jsx-filename-extension": ["error", { extensions: [".tsx", ".jsx"] }],
|
|
||||||
"react/react-in-jsx-scope": "off",
|
|
||||||
"react/prop-types": "off",
|
|
||||||
"react/require-default-props": "off",
|
|
||||||
"import/extensions": [
|
|
||||||
"error",
|
|
||||||
"ignorePackages",
|
|
||||||
{
|
|
||||||
ts: "never",
|
|
||||||
tsx: "never",
|
|
||||||
js: "never",
|
|
||||||
jsx: "never",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"import/order": "off",
|
|
||||||
"prettier/prettier": "error",
|
|
||||||
"no-unused-vars": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
argsIgnorePattern: "^_",
|
|
||||||
varsIgnorePattern: "^_",
|
|
||||||
caughtErrorsIgnorePattern: "^_",
|
|
||||||
destructuredArrayIgnorePattern: "^_",
|
|
||||||
ignoreRestSiblings: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"no-console": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
allow: ["warn", "error"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
11
.gitattributes
vendored
11
.gitattributes
vendored
|
|
@ -1,11 +0,0 @@
|
||||||
* text=auto eol=lf
|
|
||||||
|
|
||||||
*.blade.php diff=html
|
|
||||||
*.css diff=css
|
|
||||||
*.html diff=html
|
|
||||||
*.md diff=markdown
|
|
||||||
*.php diff=php
|
|
||||||
|
|
||||||
/.github export-ignore
|
|
||||||
CHANGELOG.md export-ignore
|
|
||||||
.styleci.yml export-ignore
|
|
||||||
40
.gitignore
vendored
40
.gitignore
vendored
|
|
@ -1,40 +0,0 @@
|
||||||
*.log
|
|
||||||
.DS_Store
|
|
||||||
.env
|
|
||||||
.env.backup
|
|
||||||
.env.production
|
|
||||||
.phpactor.json
|
|
||||||
.phpunit.result.cache
|
|
||||||
/.fleet
|
|
||||||
/.idea
|
|
||||||
/.nova
|
|
||||||
/.phpunit.cache
|
|
||||||
/.vscode
|
|
||||||
/.zed
|
|
||||||
/auth.json
|
|
||||||
/node_modules
|
|
||||||
/public/build
|
|
||||||
/public/hot
|
|
||||||
/public/storage
|
|
||||||
/storage/*.key
|
|
||||||
/storage/pail
|
|
||||||
/vendor
|
|
||||||
Homestead.json
|
|
||||||
Homestead.yaml
|
|
||||||
Thumbs.db
|
|
||||||
.npmrc
|
|
||||||
|
|
||||||
# Favicons in public (generated by Vite from resources/favicons)
|
|
||||||
/public/favicon-back
|
|
||||||
/public/favicon.*
|
|
||||||
/public/favicon-*.png
|
|
||||||
/public/apple-touch-icon*.png
|
|
||||||
/public/apple-icon-*.png
|
|
||||||
/public/android-chrome-*.png
|
|
||||||
/public/android-icon-*.png
|
|
||||||
/public/web-app-manifest-*.png
|
|
||||||
/public/ms-icon-*.png
|
|
||||||
/public/site.webmanifest
|
|
||||||
/public/manifest.json
|
|
||||||
|
|
||||||
/docker-compose.yml
|
|
||||||
250
.gitlab-ci.yml
250
.gitlab-ci.yml
|
|
@ -1,221 +1,35 @@
|
||||||
stages:
|
.docker-build:
|
||||||
- install
|
stage: build
|
||||||
- lint
|
image:
|
||||||
- test
|
name: gcr.io/kaniko-project/executor:v1.23.2-debug
|
||||||
- security
|
entrypoint: [ "" ]
|
||||||
- build
|
script:
|
||||||
- deploy
|
- /kaniko/executor --context "${CI_PROJECT_DIR}" --build-arg "GITLAB_USER=${CI_DEPLOY_USER}" --build-arg "GITLAB_PASSWORD=${CI_DEPLOY_PASSWORD}" --dockerfile "${CI_PROJECT_DIR}/Dockerfile" --destination "${IMAGE_TAG}" --cache=true --cache-ttl 24h
|
||||||
|
allow_failure: false
|
||||||
|
|
||||||
|
docker-build:staging:
|
||||||
|
extends: .docker-build
|
||||||
|
variables:
|
||||||
|
IMAGE_TAG: "${CI_REGISTRY_IMAGE}:s-${CI_COMMIT_SHA}"
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH == "trunk"
|
||||||
|
when: manual
|
||||||
|
allow_failure: true
|
||||||
|
|
||||||
|
docker-build:latest:
|
||||||
|
extends: .docker-build
|
||||||
|
variables:
|
||||||
|
IMAGE_TAG: "${CI_REGISTRY_IMAGE}:p-${CI_COMMIT_SHA}"
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master"
|
||||||
|
when: manual
|
||||||
|
allow_failure: true
|
||||||
|
|
||||||
include:
|
include:
|
||||||
- project: "ldc-web/shared-ci"
|
- project: 'ldc-web/shared-ci'
|
||||||
ref: "main"
|
ref: 'main'
|
||||||
file: "security/composer.yml"
|
file: 'deploy/deploy.yml'
|
||||||
- project: "ldc-web/shared-ci"
|
|
||||||
ref: "main"
|
|
||||||
file: "build/docker-multistage.yml"
|
|
||||||
- project: "ldc-web/shared-ci"
|
|
||||||
ref: "main"
|
|
||||||
file: "test/test.yml"
|
|
||||||
- project: "ldc-web/shared-ci"
|
|
||||||
ref: "main"
|
|
||||||
file: "deploy/deploy.yml"
|
|
||||||
|
|
||||||
# * LINT --------------------------------------------------------------------
|
stages:
|
||||||
|
- build
|
||||||
.php-lint-base: &php-lint-base
|
- deploy
|
||||||
image: $CI_REGISTRY/ldc-web/projets/ldc-web-images-docker/uwil-ci-8.5:latest
|
|
||||||
variables:
|
|
||||||
COMPOSER_CACHE_DIR: ".composer-cache"
|
|
||||||
CACHE_STORE: "array"
|
|
||||||
DB_CONNECTION: "sqlite"
|
|
||||||
DB_DATABASE: ":memory:"
|
|
||||||
cache:
|
|
||||||
key:
|
|
||||||
files:
|
|
||||||
- composer.lock
|
|
||||||
prefix: "composer"
|
|
||||||
policy: pull
|
|
||||||
paths:
|
|
||||||
- .composer-cache/
|
|
||||||
- vendor/
|
|
||||||
before_script:
|
|
||||||
- composer config gitlab-token.lab.s-web.fr $CI_DEPLOY_USER $CI_DEPLOY_PASSWORD
|
|
||||||
- composer install --no-scripts --no-interaction --no-ansi --no-progress --prefer-dist
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
- if: $CI_COMMIT_BRANCH == "develop"
|
|
||||||
|
|
||||||
parallel-lint:
|
|
||||||
<<: *php-lint-base
|
|
||||||
stage: lint
|
|
||||||
script:
|
|
||||||
- vendor/bin/parallel-lint --exclude vendor --exclude storage --exclude bootstrap --exclude node_modules .
|
|
||||||
allow_failure: false
|
|
||||||
|
|
||||||
pint:
|
|
||||||
<<: *php-lint-base
|
|
||||||
stage: lint
|
|
||||||
script:
|
|
||||||
- vendor/bin/pint --test
|
|
||||||
allow_failure: false
|
|
||||||
|
|
||||||
phpcs:
|
|
||||||
<<: *php-lint-base
|
|
||||||
stage: lint
|
|
||||||
script:
|
|
||||||
- vendor/bin/phpcs --standard=phpcs.xml --extensions=php --ignore=vendor/*,storage/*,bootstrap/*,node_modules/* --report=junit --report-file=phpcs-report.xml
|
|
||||||
artifacts:
|
|
||||||
when: on_failure
|
|
||||||
expose_as: 'phpcs'
|
|
||||||
paths:
|
|
||||||
- phpcs-report.xml
|
|
||||||
reports:
|
|
||||||
junit: phpcs-report.xml
|
|
||||||
allow_failure: false
|
|
||||||
|
|
||||||
phpstan:
|
|
||||||
<<: *php-lint-base
|
|
||||||
stage: lint
|
|
||||||
variables:
|
|
||||||
PHP_MEMORY_LIMIT: "512M"
|
|
||||||
COMPOSER_CACHE_DIR: ".composer-cache"
|
|
||||||
CACHE_STORE: "array"
|
|
||||||
DB_CONNECTION: "sqlite"
|
|
||||||
DB_DATABASE: ":memory:"
|
|
||||||
script:
|
|
||||||
- php -d memory_limit=512M vendor/bin/phpstan --error-format=junit > phpstan-report.xml
|
|
||||||
artifacts:
|
|
||||||
when: on_failure
|
|
||||||
expose_as: 'phpstan'
|
|
||||||
paths:
|
|
||||||
- phpstan-report.xml
|
|
||||||
reports:
|
|
||||||
junit: phpstan-report.xml
|
|
||||||
allow_failure: false
|
|
||||||
|
|
||||||
eslint:
|
|
||||||
stage: lint
|
|
||||||
image: oven/bun:1
|
|
||||||
variables:
|
|
||||||
BUN_INSTALL_CACHE_DIR: ".bun-cache"
|
|
||||||
cache:
|
|
||||||
key:
|
|
||||||
files:
|
|
||||||
- bun.lock
|
|
||||||
prefix: "bun"
|
|
||||||
policy: pull
|
|
||||||
paths:
|
|
||||||
- .bun-cache/
|
|
||||||
before_script:
|
|
||||||
- echo "@ldc-web:registry=https://${CI_SERVER_HOST}/api/v4/packages/npm/" >> .npmrc
|
|
||||||
- echo "//${CI_SERVER_HOST}/api/v4/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
|
||||||
- echo "@uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/" >> .npmrc
|
|
||||||
- echo "//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
|
||||||
- bun install --no-save
|
|
||||||
script:
|
|
||||||
- bun run eslint-ci --quiet --output-file=eslint-junit.xml
|
|
||||||
artifacts:
|
|
||||||
when: on_failure
|
|
||||||
expose_as: 'eslint'
|
|
||||||
paths:
|
|
||||||
- eslint-junit.xml
|
|
||||||
reports:
|
|
||||||
junit: eslint-junit.xml
|
|
||||||
allow_failure: false
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
- if: $CI_COMMIT_BRANCH == "develop"
|
|
||||||
|
|
||||||
stylelint:
|
|
||||||
stage: lint
|
|
||||||
image: oven/bun:1
|
|
||||||
variables:
|
|
||||||
BUN_INSTALL_CACHE_DIR: ".bun-cache"
|
|
||||||
cache:
|
|
||||||
key:
|
|
||||||
files:
|
|
||||||
- bun.lock
|
|
||||||
prefix: "bun"
|
|
||||||
policy: pull
|
|
||||||
paths:
|
|
||||||
- .bun-cache/
|
|
||||||
before_script:
|
|
||||||
- echo "@ldc-web:registry=https://${CI_SERVER_HOST}/api/v4/packages/npm/" >> .npmrc
|
|
||||||
- echo "//${CI_SERVER_HOST}/api/v4/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
|
||||||
- echo "@uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/" >> .npmrc
|
|
||||||
- echo "//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
|
||||||
- bun install --no-save
|
|
||||||
script:
|
|
||||||
- bun run stylelint-ci --quiet --output-file=stylelint-junit.xml
|
|
||||||
artifacts:
|
|
||||||
when: on_failure
|
|
||||||
expose_as: 'stylelint'
|
|
||||||
paths:
|
|
||||||
- stylelint-junit.xml
|
|
||||||
reports:
|
|
||||||
junit: stylelint-junit.xml
|
|
||||||
allow_failure: false
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
- if: $CI_COMMIT_BRANCH == "develop"
|
|
||||||
|
|
||||||
# * TESTS -------------------------------------------------------------------
|
|
||||||
|
|
||||||
bun-build-test:
|
|
||||||
stage: test
|
|
||||||
image: oven/bun:1
|
|
||||||
variables:
|
|
||||||
BUN_INSTALL_CACHE_DIR: ".bun-cache"
|
|
||||||
cache:
|
|
||||||
key:
|
|
||||||
files:
|
|
||||||
- bun.lock
|
|
||||||
prefix: "bun"
|
|
||||||
policy: pull
|
|
||||||
paths:
|
|
||||||
- .bun-cache/
|
|
||||||
before_script:
|
|
||||||
- echo "@ldc-web:registry=https://${CI_SERVER_HOST}/api/v4/packages/npm/" >> .npmrc
|
|
||||||
- echo "//${CI_SERVER_HOST}/api/v4/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
|
||||||
- echo "@uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/" >> .npmrc
|
|
||||||
- echo "//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
|
||||||
- bun install --no-save
|
|
||||||
script:
|
|
||||||
- timeout 300 bun run prod
|
|
||||||
artifacts:
|
|
||||||
expose_as: 'front-build-test'
|
|
||||||
paths:
|
|
||||||
- public/build/manifest.json
|
|
||||||
allow_failure: false
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
- if: $CI_COMMIT_BRANCH == "trunk" || $CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "main"
|
|
||||||
- if: $CI_COMMIT_TAG != null
|
|
||||||
|
|
||||||
# * SECURITY ----------------------------------------------------------------
|
|
||||||
|
|
||||||
npm-security:
|
|
||||||
stage: security
|
|
||||||
image: oven/bun:1
|
|
||||||
before_script:
|
|
||||||
- echo "@ldc-web:registry=https://${CI_SERVER_HOST}/api/v4/packages/npm/" >> .npmrc
|
|
||||||
- echo "//${CI_SERVER_HOST}/api/v4/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
|
||||||
- echo "@uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/" >> .npmrc
|
|
||||||
- echo "//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
|
||||||
script:
|
|
||||||
- bun audit --json > bun-security-checker-report.json
|
|
||||||
artifacts:
|
|
||||||
when: on_failure
|
|
||||||
expose_as: 'bun-security-checker'
|
|
||||||
paths:
|
|
||||||
- bun-security-checker-report.json
|
|
||||||
allow_failure: true
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
- if: $CI_COMMIT_BRANCH == "trunk" || $CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "main"
|
|
||||||
- if: $CI_COMMIT_TAG != null
|
|
||||||
|
|
||||||
# * BUILD -------------------------------------------------------------------
|
|
||||||
|
|
||||||
.docker-build:
|
|
||||||
dependencies: []
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
## Comportement constaté
|
|
||||||
|
|
||||||
|
|
||||||
## Comportement attendu
|
|
||||||
|
|
||||||
|
|
||||||
## Solution envisagée
|
|
||||||
|
|
||||||
|
|
||||||
## Étapes pour la reproduction du bug
|
|
||||||
*
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
## Présentation du besoin
|
|
||||||
|
|
||||||
|
|
||||||
## Implémentation
|
|
||||||
|
|
||||||
|
|
||||||
## Maquettes, documentations...
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
npx commitlint --config node_modules/@uwil/husky-config/commitlint.config.js --edit $1
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
{
|
|
||||||
"extends": [
|
|
||||||
"stylelint-config-standard-scss",
|
|
||||||
"stylelint-config-sass-guidelines",
|
|
||||||
"stylelint-config-idiomatic-order"
|
|
||||||
],
|
|
||||||
"plugins": ["stylelint-scss"],
|
|
||||||
"files": ["**/*.css", "**/*.scss"],
|
|
||||||
"ignoreFiles": [
|
|
||||||
"**/vendor/*.css",
|
|
||||||
"**/vendor/**/*.css",
|
|
||||||
"**/vendor/*.scss",
|
|
||||||
"**/vendor/**/*.scss",
|
|
||||||
"**/vendor/*.sass",
|
|
||||||
"**/vendor/**/*.sass",
|
|
||||||
"**/packages/*.css",
|
|
||||||
"**/packages/**/*.css",
|
|
||||||
"**/packages/*.scss",
|
|
||||||
"**/packages/**/*.scss",
|
|
||||||
"**/packages/*.sass",
|
|
||||||
"**/packages/**/*.sass"
|
|
||||||
],
|
|
||||||
"customSyntax": "postcss-scss",
|
|
||||||
"rules": {
|
|
||||||
"declaration-property-value-no-unknown": true,
|
|
||||||
"selector-max-id": null,
|
|
||||||
"color-no-invalid-hex": true,
|
|
||||||
"order/properties-alphabetical-order": null,
|
|
||||||
"max-nesting-depth": 5,
|
|
||||||
"selector-max-compound-selectors": 5,
|
|
||||||
"scss/double-slash-comment-empty-line-before": [
|
|
||||||
"always",
|
|
||||||
{
|
|
||||||
"ignore": [
|
|
||||||
"between-comments",
|
|
||||||
"stylelint-commands",
|
|
||||||
"inside-block"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"at-rule-no-unknown": null,
|
|
||||||
"scss/at-rule-no-unknown": [
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
"ignoreAtRules": [
|
|
||||||
"tailwind",
|
|
||||||
"apply",
|
|
||||||
"variants",
|
|
||||||
"responsive",
|
|
||||||
"screen",
|
|
||||||
"layer"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
5
.vscode/extensions.json
vendored
5
.vscode/extensions.json
vendored
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"emeraldwalk.runonsave"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
109
.vscode/settings.json
vendored
109
.vscode/settings.json
vendored
|
|
@ -1,109 +0,0 @@
|
||||||
{
|
|
||||||
"workbench.colorCustomizations": {
|
|
||||||
"activityBar.activeBackground": "#b0981c",
|
|
||||||
"activityBar.activeBorder": "#15202b",
|
|
||||||
"activityBar.background": "#b0981c",
|
|
||||||
"activityBar.foreground": "#15202b",
|
|
||||||
"activityBar.inactiveForeground": "#15202b99",
|
|
||||||
"activityBarBadge.background": "#91edde",
|
|
||||||
"activityBarBadge.foreground": "#15202b",
|
|
||||||
"sash.hoverBorder": "#b0981c",
|
|
||||||
"statusBar.background": "#847215",
|
|
||||||
"statusBar.foreground": "#e7e7e7",
|
|
||||||
"statusBarItem.hoverBackground": "#b0981c",
|
|
||||||
"statusBarItem.remoteBackground": "#1fc0a6",
|
|
||||||
"statusBarItem.remoteForeground": "#e7e7e7",
|
|
||||||
"titleBar.activeBackground": "#847215",
|
|
||||||
"titleBar.activeForeground": "#e7e7e7",
|
|
||||||
"titleBar.inactiveBackground": "#84721599",
|
|
||||||
"titleBar.inactiveForeground": "#e7e7e799",
|
|
||||||
"commandCenter.border": "#e7e7e799",
|
|
||||||
"activityBarTop.activeBackground": "#b0981c",
|
|
||||||
"activityBarTop.activeBorder": "#15202b",
|
|
||||||
"activityBarTop.background": "#b0981c",
|
|
||||||
"activityBarTop.foreground": "#15202b",
|
|
||||||
"activityBarTop.inactiveForeground": "#15202b99",
|
|
||||||
"commandCenter.foreground": "#e7e7e7",
|
|
||||||
"statusBar.debuggingBackground": "#847215",
|
|
||||||
"statusBar.debuggingForeground": "#e7e7e7",
|
|
||||||
"statusBar.inactiveBackground": "#847215"
|
|
||||||
},
|
|
||||||
"peacock.color": "#847215",
|
|
||||||
"peacock.remoteColor": "#847215",
|
|
||||||
"intelephense.environment.phpVersion": "8.5.0",
|
|
||||||
"intelephense.environment.includePaths": [
|
|
||||||
"./.intellipest",
|
|
||||||
"./vendor/fakerphp/faker",
|
|
||||||
"./vendor/pestphp",
|
|
||||||
"./vendor/phpunit/phpunit",
|
|
||||||
"./vendor/laravel"
|
|
||||||
],
|
|
||||||
"phpstan.binCommand": [
|
|
||||||
"${workspaceFolder}/vendor/bin/phpstan"
|
|
||||||
],
|
|
||||||
"laravel-pint.enable": true,
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "html", "vue"],
|
|
||||||
"[html]": {
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.formatOnPaste": true,
|
|
||||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint",
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll.eslint": "always"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"[blade]": {
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.formatOnType": false
|
|
||||||
},
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.autoIndent": "full",
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll.eslint": "always"
|
|
||||||
},
|
|
||||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
|
||||||
"editor.detectIndentation": false,
|
|
||||||
"editor.formatOnSave": true
|
|
||||||
},
|
|
||||||
"[scss]": {
|
|
||||||
"editor.defaultFormatter": "stylelint.vscode-stylelint",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll.stylelint": "always"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.autoIndent": "full",
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll.eslint": "always"
|
|
||||||
},
|
|
||||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
|
||||||
"editor.detectIndentation": false,
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.formatOnSaveMode": "file"
|
|
||||||
},
|
|
||||||
"[php]": {
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.formatOnSaveMode": "file"
|
|
||||||
},
|
|
||||||
"[vue]": {
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll.eslint": "always"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"conventionalCommits.scopes": [
|
|
||||||
"migration"
|
|
||||||
],
|
|
||||||
"emeraldwalk.runonsave": {
|
|
||||||
"autoClearConsole": false,
|
|
||||||
"commands": [
|
|
||||||
{
|
|
||||||
"match": "\\.php$",
|
|
||||||
"isAsync": true,
|
|
||||||
"cmd": "${workspaceFolder}/vendor/bin/pint ${file}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"LaravelExtraIntellisense.phpCommand": "docker exec -w /app/srcs -t ovoteam-$(whoami)-apache-php-1 php -r \"{code}\"",
|
|
||||||
"LaravelExtraIntellisense.basePathForCode": "/app/srcs"
|
|
||||||
}
|
|
||||||
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -1,12 +0,0 @@
|
||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to `laravel-project` will be documented in this file.
|
|
||||||
|
|
||||||
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
||||||
- This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
|
||||||
- Commits respect [Conventionnal commits](https://www.conventionalcommits.org/en/v1.0.0/) & use [Gitmoji](https://gitmoji.dev/).
|
|
||||||
|
|
||||||
## **[v1.0.0] - 01/01/2026**
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- feat: 🎉 working project
|
|
||||||
43
DOMAIN.md
43
DOMAIN.md
|
|
@ -1,43 +0,0 @@
|
||||||
## Domaine Métier et Contraintes : Les Produits
|
|
||||||
|
|
||||||
Le catalogue de produits repose sur une segmentation précise nécessitant obligatoirement 5 critères d'identification[cite: 1, 2]. Les produits se déclinent sous de très nombreux formats et conditionnements (colis, barquettes, bidons, poches, sachets, seaux, verrines, etc.)[cite: 2].
|
|
||||||
|
|
||||||
### Caractéristiques et filtres des produits
|
|
||||||
|
|
||||||
| Attribut | Valeurs métier définies | Contraintes de sélection |
|
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| **Catégories / Gammes** | Œufs liquides petit conditionnement, Œufs liquides industrie, Œufs liquides pour la grande distribution, Œufs durs, Œufs brouillés, Œufs pochés, Omelettes et Gammes traiteur et desserts[cite: 1, 2]. | 1 seule par produit[cite: 1, 2]. |
|
|
||||||
| **Technologies** | Frais, Surgelé, Ambiant[cite: 2]. | 1 seule par produit[cite: 2]. |
|
|
||||||
| **Modes d'élevage** | Plein air, Bio, Sol, Conventionnel et Label Rouge[cite: 1, 2]. | Minimum 1, maximum illimité (X)[cite: 1]. |
|
|
||||||
| **Métiers (Cibles)** | Hôtellerie, Restauration rapide et snacking, Boulangerie pâtisserie, Restauration collective, Industrie agro-alimentaire, Grande distribution et Restauration à table[cite: 1, 2]. | Minimum 1, maximum illimité (X)[cite: 1]. |
|
|
||||||
| **Égalim** | Oui, Non[cite: 2]. | Obligatoire ("Oui" ou "Non")[cite: 2]. |
|
|
||||||
|
|
||||||
### Contraintes d'affichage (Fiche Produit)
|
|
||||||
|
|
||||||
* Les champs dédiés aux valeurs nutritionnelles et le fichier PDF ne doivent pas s'afficher en front-office si aucune information n'est complétée[cite: 1].
|
|
||||||
* Une section « Consignes de conservation avant ouverture » doit être ajoutée en dessous de la section détails/avantages[cite: 1].
|
|
||||||
* Une section « Consignes de conservation après ouverture » doit être ajoutée sous la section "avant ouverture", mais ne doit pas s'afficher si elle n'est pas complétée[cite: 1].
|
|
||||||
* Un slider de recettes doit être implémenté en bas de page avec 9 slides[cite: 1].
|
|
||||||
* Ce slider doit afficher en premier les recettes associées au produit, puis compléter avec d'autres recettes de manière aléatoire[cite: 1].
|
|
||||||
* Aucune liaison avec INCO n'est à prévoir (pas de code INCO)[cite: 1].
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Domaine Métier et Contraintes : Les Recettes
|
|
||||||
|
|
||||||
Les recettes interagissent avec les produits et sont soumises à 4 filtres spécifiques pour faciliter la navigation des utilisateurs[cite: 2].
|
|
||||||
|
|
||||||
### Caractéristiques et filtres des recettes
|
|
||||||
|
|
||||||
| Attribut | Valeurs métier définies | Contraintes de sélection |
|
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| **Gammes de produits** | Identiques aux gammes de produits : Œufs liquides (petit conditionnement, industrie, grande distribution), Œufs durs, brouillés, pochés, Omelettes, traiteur et desserts[cite: 1, 2]. | 1 seule gamme par recette[cite: 1]. |
|
|
||||||
| **Moments de consommation** | Petit déjeuner / brunch, Déjeuner / dîner, Snacking, Dessert / pâtisserie[cite: 1, 2]. | 1 seul moment par recette[cite: 1]. |
|
|
||||||
| **Inspirations** | Cuisine du monde, cuisine française, cuisine asiatique, cuisine protéinée, Snacking, Sucré, Brunch[cite: 1, 2]. | 1 seule inspiration par recette[cite: 1]. |
|
|
||||||
| **Saisons** | Printemps/été, Automne/hiver, Toute l'année[cite: 1, 2]. | 1 seule saison par recette[cite: 1]. |
|
|
||||||
|
|
||||||
### Contraintes d'affichage (Fiche Recette)
|
|
||||||
|
|
||||||
* Le champ « astuces et conseils » ne doit pas s'afficher en front-office si aucune information n'est complétée[cite: 1].
|
|
||||||
* Un slider des produits doit être implémenté en bas de page avec 6 slides[cite: 1].
|
|
||||||
* Ce slider doit afficher en premier les produits associés à la recette, puis afficher d'autres produits en aléatoire pour compléter[cite: 1].
|
|
||||||
53
Dockerfile
53
Dockerfile
|
|
@ -1,55 +1,4 @@
|
||||||
ARG PHP_8_5_TAG=latest
|
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/ldcweb-production-8.3:170625-101047
|
||||||
|
|
||||||
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/uwil-ci-8.5:latest AS vendor
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ARG GITLAB_USER
|
|
||||||
ARG GITLAB_PASSWORD
|
|
||||||
|
|
||||||
COPY database/ database/
|
|
||||||
|
|
||||||
COPY composer.json composer.json
|
|
||||||
COPY composer.lock composer.lock
|
|
||||||
|
|
||||||
RUN composer config gitlab-token.lab.s-web.fr "${GITLAB_USER}" "${GITLAB_PASSWORD}"
|
|
||||||
RUN composer install \
|
|
||||||
--ignore-platform-reqs \
|
|
||||||
--no-interaction \
|
|
||||||
--no-plugins \
|
|
||||||
--no-scripts \
|
|
||||||
--no-dev \
|
|
||||||
--prefer-dist
|
|
||||||
|
|
||||||
FROM oven/bun:1.4.4-alpine AS frontend
|
|
||||||
|
|
||||||
ARG GITLAB_PASSWORD
|
|
||||||
|
|
||||||
RUN mkdir -p /app/public/build
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package.json bun.lock* postcss.config.js tailwind.config.js tsconfig.json vite.config.ts .eslintrc.cjs .stylelintrc.json /app/
|
|
||||||
COPY resources/ /app/resources/
|
|
||||||
COPY --from=vendor /app/vendor/ /app/vendor/
|
|
||||||
|
|
||||||
RUN echo "@ldc-web:registry=https://lab.s-web.fr/api/v4/packages/npm/" >> .npmrc && \
|
|
||||||
echo "//lab.s-web.fr/api/v4/packages/npm/:_authToken=${GITLAB_PASSWORD}" >> .npmrc && \
|
|
||||||
echo "@uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/" >> .npmrc && \
|
|
||||||
echo "//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken=${GITLAB_PASSWORD}" >> .npmrc
|
|
||||||
|
|
||||||
RUN bun install --frozen-lockfile && bun run prod
|
|
||||||
|
|
||||||
ARG PHP_8_5_TAG
|
|
||||||
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/uwil-production-8.5:${PHP_8_5_TAG}
|
|
||||||
|
|
||||||
ARG USER=www-data
|
ARG USER=www-data
|
||||||
|
|
||||||
COPY --chown=${USER}:${USER} . /app/srcs
|
COPY --chown=${USER}:${USER} . /app/srcs
|
||||||
COPY --chown=${USER}:${USER} ./resources/favicons/front/ /app/srcs/public/
|
|
||||||
COPY --chown=${USER}:${USER} ./resources/favicons/back/ /app/srcs/public/back/
|
|
||||||
COPY --from=vendor --chown=${USER}:${USER} /app/vendor/ /app/srcs/vendor/
|
|
||||||
COPY --from=frontend --chown=${USER}:${USER} /app/public/build/ /app/srcs/public/build/
|
|
||||||
|
|
||||||
USER root
|
|
||||||
RUN chmod -R 755 /app/srcs/storage
|
|
||||||
USER ${USER}
|
USER ${USER}
|
||||||
122
README.md
122
README.md
|
|
@ -1,33 +1,111 @@
|
||||||
# Table of contents
|
# OVO TEAM
|
||||||
|
|
||||||
- [Table of contents](#table-of-contents)
|
## Instalation
|
||||||
- [Frameworks, Platforms and Libraries](#frameworks-platforms-and-libraries)
|
|
||||||
- [Documentation](#documentation)
|
|
||||||
- [Changelog](#changelog)
|
|
||||||
- [Copyright and License](#copyright-and-license)
|
|
||||||
|
|
||||||
# Frameworks, Platforms and Libraries
|
Copier `.env` vers `.env.example` et remplir les informations.
|
||||||
|
|
||||||
<!-- https://github.com/Ileriayo/markdown-badges -->
|
## Déployer
|
||||||
|
|
||||||
[](https://sass-lang.com/)
|
- Apres git pull sur l'envirronnement désiré
|
||||||
[](https://tailwindcss.com/)
|
- Envoyer les éléments uploads si nécéssaire (Ne devrais pas étre le cas)
|
||||||
[](https://vite.dev/)
|
- Executer la partie `Compiler le front`
|
||||||
[](https://www.php.net/)
|
|
||||||
[](https://laravel.com/docs/master)
|
|
||||||
|
|
||||||
# Documentation
|
## Faire une mise à jour
|
||||||
|
|
||||||
```php
|
Commenter dans `public/.htaccess` la partie `# Hide update.php`, puis accéder à ce fichier via la navigateur
|
||||||
composer i/npm i
|
|
||||||
```
|
|
||||||
|
|
||||||
# Changelog
|
## Fichier uploads
|
||||||
|
|
||||||
Latest version v1.0.0.
|
Au sein de `public/sites/default/files`
|
||||||
|
|
||||||
See the [CHANGELOG.md](CHANGELOG.md) file for details.
|
Les dossiers :
|
||||||
|
|
||||||
# Copyright and License
|
- `inline-images` Pour les contenus insérés dans les WYSIWYG
|
||||||
|
- les dossiers `20*` comme `2023-05` par exemple pour les media uploadés via la création de pages
|
||||||
|
- `styles` qui contienent des images uploadés de dimension spécifiques
|
||||||
|
|
||||||
Copyright (c) 2025 and beyond UWIL.
|
## Compiler le front
|
||||||
|
|
||||||
|
`node_modules/gulp/bin/gulp.js -f public/themes/custom/custtheme/gulpfile.js`
|
||||||
|
|
||||||
|
Deactiver `Aggreger les fichiers CSS et javascript` au sein de `/fr/admin/config/development/performance`
|
||||||
|
|
||||||
|
Cliquer sur `Effacer tous les caches` si vous reactivez l'option
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Original README
|
||||||
|
|
||||||
|
Drupal is an open source content management platform supporting a variety of
|
||||||
|
websites ranging from personal weblogs to large community-driven websites. For
|
||||||
|
more information, visit the Drupal website, [Drupal.org][Drupal.org], and join
|
||||||
|
the [Drupal community][Drupal community].
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Drupal is developed on [Drupal.org][Drupal.org], the home of the international
|
||||||
|
Drupal community since 2001!
|
||||||
|
|
||||||
|
[Drupal.org][Drupal.org] hosts Drupal's [GitLab repository][GitLab repository],
|
||||||
|
its [issue queue][issue queue], and its [documentation][documentation]. Before
|
||||||
|
you start working on code, be sure to search the [issue queue][issue queue] and
|
||||||
|
create an issue if your aren't able to find an existing issue.
|
||||||
|
|
||||||
|
Every issue on Drupal.org automatically creates a new community-accessible fork
|
||||||
|
that you can contribute to. Learn more about the code contribution process on
|
||||||
|
the [Issue forks & merge requests page][issue forks].
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
For a brief introduction, see [USAGE.txt](/core/USAGE.txt). You can also find
|
||||||
|
guides, API references, and more by visiting Drupal's [documentation
|
||||||
|
page][documentation].
|
||||||
|
|
||||||
|
You can quickly extend Drupal's core feature set by installing any of its
|
||||||
|
[thousands of free and open source modules][modules]. With Drupal and its
|
||||||
|
module ecosystem, you can often build most or all of what your project needs
|
||||||
|
before writing a single line of code.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
Drupal keeps detailed [change records][changelog]. You can search Drupal's
|
||||||
|
changes for a record of every notable breaking change and new feature since
|
||||||
|
2011.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
For a list of security announcements, see the [Security advisories
|
||||||
|
page][Security advisories] (available as [an RSS feed][security RSS]). This
|
||||||
|
page also describes how to subscribe to these announcements via email.
|
||||||
|
|
||||||
|
For information about the Drupal security process, or to find out how to report
|
||||||
|
a potential security issue to the Drupal security team, see the [Security team
|
||||||
|
page][security team].
|
||||||
|
|
||||||
|
## Need a helping hand?
|
||||||
|
|
||||||
|
Visit the [Support page][support] or browse [over a thousand Drupal
|
||||||
|
providers][service providers] offering design, strategy, development, and
|
||||||
|
hosting services.
|
||||||
|
|
||||||
|
## Legal matters
|
||||||
|
|
||||||
|
Know your rights when using Drupal by reading Drupal core's
|
||||||
|
[license](/core/LICENSE.txt).
|
||||||
|
|
||||||
|
Learn about the [Drupal trademark and logo policy here][trademark].
|
||||||
|
|
||||||
|
[Drupal.org]: https://www.drupal.org
|
||||||
|
[Drupal community]: https://www.drupal.org/community
|
||||||
|
[GitLab repository]: https://git.drupalcode.org/project/drupal
|
||||||
|
[issue queue]: https://www.drupal.org/project/issues/drupal
|
||||||
|
[issue forks]: https://www.drupal.org/drupalorg/docs/gitlab-integration/issue-forks-merge-requests
|
||||||
|
[documentation]: https://www.drupal.org/documentation
|
||||||
|
[changelog]: https://www.drupal.org/list-changes/drupal
|
||||||
|
[modules]: https://www.drupal.org/project/project_module
|
||||||
|
[security advisories]: https://www.drupal.org/security
|
||||||
|
[security RSS]: https://www.drupal.org/security/rss.xml
|
||||||
|
[security team]: https://www.drupal.org/drupal-security-team
|
||||||
|
[service providers]: https://www.drupal.org/drupal-services
|
||||||
|
[support]: https://www.drupal.org/support
|
||||||
|
[trademark]: https://www.drupal.com/trademark
|
||||||
|
|
|
||||||
32
RGPD.md
32
RGPD.md
|
|
@ -1,32 +0,0 @@
|
||||||
# RGPD
|
|
||||||
|
|
||||||
## Site
|
|
||||||
|
|
||||||
### En base de données
|
|
||||||
|
|
||||||
| Table | Colonne | Contenu | Utilisation |
|
|
||||||
| ----- | ------- | ------- | ----------- |
|
|
||||||
| activity_logs | user_id | Identifiant de l'utilisateur (back) | Tracer les modifications dans le BO |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| users | first_name | Prénom | Affichage à différents endroits du BO |
|
|
||||||
| users | last_name | Nom | Affichage à différents endroits du BO |
|
|
||||||
| users | email | Adresse mail | Identifiant, sert à la connexion au BO |
|
|
||||||
|
|
||||||
### Formulaire de contact (marketing)
|
|
||||||
|
|
||||||
| Donnée | Contenu |
|
|
||||||
| ------ | ------- |
|
|
||||||
| Nom | Nom de la personne |
|
|
||||||
| Prénom | Prénom de la personne |
|
|
||||||
| Téléphone | Numéro de téléphone de la personne |
|
|
||||||
| Adresse E-mail | Adresse email de la personne |
|
|
||||||
| Nom de l'entreprise | Nom de l'entreprise de la personne |
|
|
||||||
| Secteur d'activité | Secteur d'activité de la personne |
|
|
||||||
|
|
||||||
## Cookies
|
|
||||||
|
|
||||||
| Service | Utilité |
|
|
||||||
| ------- | ------- |
|
|
||||||
| Matomo | Mesure d'audience |
|
|
||||||
| Google Recaptcha | Empêcher la soumission de formulaire par des robots |
|
|
||||||
| Youtube | Visionner du contenu disponible sur la plateforme Youtube |
|
|
||||||
31184
_ide_helper.php
31184
_ide_helper.php
File diff suppressed because it is too large
Load diff
|
|
@ -1,45 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Casts;
|
|
||||||
|
|
||||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
|
|
||||||
|
|
||||||
/** @implements CastsAttributes<StorageFilePath|null, StorageFilePath|string|null> */
|
|
||||||
class FilePathCast implements CastsAttributes
|
|
||||||
{
|
|
||||||
/** @param array<string, mixed> $attributes */
|
|
||||||
public function get(Model $model, string $key, mixed $value, array $attributes): ?StorageFilePath
|
|
||||||
{
|
|
||||||
if ($value === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! is_string($value)) {
|
|
||||||
throw new \InvalidArgumentException('The given value is not a valid file path string.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return new StorageFilePath($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param array<string, mixed> $attributes */
|
|
||||||
public function set(Model $model, string $key, mixed $value, array $attributes): ?string
|
|
||||||
{
|
|
||||||
if ($value === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($value instanceof StorageFilePath) {
|
|
||||||
return $value->value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_string($value)) {
|
|
||||||
return (new StorageFilePath($value))->value;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new \InvalidArgumentException('The given value is not a valid StorageFilePath instance or path string.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Casts;
|
|
||||||
|
|
||||||
use Domain\Models\ValueObjects\Products\NutritionalValue;
|
|
||||||
use Domain\Models\ValueObjects\Products\NutritionalValues;
|
|
||||||
use Domain\Models\ValueObjects\Products\NutritionnalUnit;
|
|
||||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @implements CastsAttributes<NutritionalValues|null, NutritionalValues|array<string, mixed>|string|null>
|
|
||||||
*/
|
|
||||||
class NutritionalValuesCast implements CastsAttributes
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $attributes
|
|
||||||
*/
|
|
||||||
public function get(Model $model, string $key, mixed $value, array $attributes): ?NutritionalValues
|
|
||||||
{
|
|
||||||
if ($value === null || $value === '' || $value === '[]') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! is_string($value)) {
|
|
||||||
throw new \InvalidArgumentException('The given value is not a string.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = json_decode($value, true);
|
|
||||||
|
|
||||||
if (! is_array($data)) {
|
|
||||||
throw new \InvalidArgumentException('The given value is not a valid JSON array.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($data === []) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->fromArray($data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $attributes
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
public function set(Model $model, string $key, mixed $value, array $attributes): array
|
|
||||||
{
|
|
||||||
if ($value === null || $value === [] || $value === '' || $value === '[]') {
|
|
||||||
return [$key => '[]'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_string($value)) {
|
|
||||||
$value = json_decode($value, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_array($value)) {
|
|
||||||
$value = $this->fromArray($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($value === null) {
|
|
||||||
return [$key => '[]'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $value instanceof NutritionalValues) {
|
|
||||||
throw new \InvalidArgumentException('The given value is not a NutritionalValues instance.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return [$key => (string) json_encode($value->toArray(), JSON_THROW_ON_ERROR)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<array-key, mixed> $data
|
|
||||||
*/
|
|
||||||
private function fromArray(array $data): ?NutritionalValues
|
|
||||||
{
|
|
||||||
if ($data === []) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$getProperty = function (string $key) use ($data): ?NutritionalValue {
|
|
||||||
$item = Arr::get($data, $key);
|
|
||||||
|
|
||||||
return is_array($item) ? $this->toNutritionalValue($item) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
$refWeightG = $getProperty('refWeightG');
|
|
||||||
|
|
||||||
if ($refWeightG === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new NutritionalValues(
|
|
||||||
refWeightG: $refWeightG,
|
|
||||||
kj: $getProperty('kj'),
|
|
||||||
kcal: $getProperty('kcal'),
|
|
||||||
fatG: $getProperty('fatG'),
|
|
||||||
satFatG: $getProperty('satFatG'),
|
|
||||||
carbG: $getProperty('carbG'),
|
|
||||||
sugarG: $getProperty('sugarG'),
|
|
||||||
fiberG: $getProperty('fiberG'),
|
|
||||||
proteinG: $getProperty('proteinG'),
|
|
||||||
saltG: $getProperty('saltG'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<array-key, mixed> $data
|
|
||||||
*/
|
|
||||||
private function toNutritionalValue(array $data): ?NutritionalValue
|
|
||||||
{
|
|
||||||
$rawItemVal = Arr::get($data, 'value');
|
|
||||||
$rawItemUnit = Arr::get($data, 'unit');
|
|
||||||
|
|
||||||
if (! is_string($rawItemVal) && ! is_numeric($rawItemVal)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! is_string($rawItemUnit) && ! $rawItemUnit instanceof NutritionnalUnit) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$value = mb_trim((string) $rawItemVal);
|
|
||||||
$unit = $rawItemUnit instanceof NutritionnalUnit
|
|
||||||
? $rawItemUnit
|
|
||||||
: NutritionnalUnit::tryFrom((string) $rawItemUnit);
|
|
||||||
|
|
||||||
if ($value === '' || $unit === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new NutritionalValue($value, $unit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Contracts;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template TInput of \App\Contracts\ActionInput
|
|
||||||
* @template TResult
|
|
||||||
*/
|
|
||||||
interface Action
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param TInput $input
|
|
||||||
* @return TResult
|
|
||||||
*/
|
|
||||||
public function __invoke(ActionInput $input);
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Contracts;
|
|
||||||
|
|
||||||
interface ActionInput
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Contracts;
|
|
||||||
|
|
||||||
final class ActionInputEmpty implements ActionInput
|
|
||||||
{
|
|
||||||
public static function create(): self
|
|
||||||
{
|
|
||||||
return new self();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Contracts\Directories;
|
|
||||||
|
|
||||||
use App\Contracts\Directory;
|
|
||||||
use App\Dto\CategoryProductsDto;
|
|
||||||
use Domain\Models\Entities\Product;
|
|
||||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
|
||||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
|
||||||
use Domain\Models\ValueObjects\Pagination;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
|
||||||
|
|
||||||
/** @extends Directory<int, \Domain\Models\Entities\Product> */
|
|
||||||
interface ProductDirectory extends Directory
|
|
||||||
{
|
|
||||||
public function find(ProductId $id): ?Product;
|
|
||||||
|
|
||||||
public function findBySlug(string $slug): ?Product;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return \Domain\Models\ValueObjects\Products\ProductSummary[]
|
|
||||||
*/
|
|
||||||
public function getAllProductsSummaries(): array;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return LengthAwarePaginator<array-key,Product>
|
|
||||||
*/
|
|
||||||
public function getPaginatedProducts(
|
|
||||||
Pagination $pagination,
|
|
||||||
string $search = '',
|
|
||||||
): LengthAwarePaginator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \Domain\Models\ValueObjects\Products\BreedingMethod[] $breedingMethods
|
|
||||||
* @param \Domain\Models\ValueObjects\Products\Technology[] $technologies
|
|
||||||
* @param \Domain\Models\ValueObjects\Products\Business[] $businesses
|
|
||||||
*/
|
|
||||||
public function getCategoryProducts(
|
|
||||||
ProductCategory $category,
|
|
||||||
array $breedingMethods = [],
|
|
||||||
array $technologies = [],
|
|
||||||
array $businesses = [],
|
|
||||||
?bool $isEgalim = null,
|
|
||||||
string $search = '',
|
|
||||||
int $limitPerCategory = 6
|
|
||||||
): ?CategoryProductsDto;
|
|
||||||
|
|
||||||
/** @return \Domain\Models\Entities\Product[] */
|
|
||||||
public function getRelatedProducts(RecipeId $recipeId, int $limit = 4): array;
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Contracts\Directories;
|
|
||||||
|
|
||||||
use App\Contracts\Directory;
|
|
||||||
use Domain\Models\Entities\Recipe;
|
|
||||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
|
||||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
|
||||||
use Domain\Models\ValueObjects\Pagination;
|
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
|
||||||
|
|
||||||
/** @extends Directory<int, \Domain\Models\Entities\Recipe> */
|
|
||||||
interface RecipeDirectory extends Directory
|
|
||||||
{
|
|
||||||
public function find(RecipeId $id): ?Recipe;
|
|
||||||
|
|
||||||
public function findBySlug(string $slug): ?Recipe;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \Domain\Models\ValueObjects\Products\ProductCategory[] $productRanges
|
|
||||||
* @param \Domain\Models\ValueObjects\Recipes\ConsumptionTime[] $consumptionTimes
|
|
||||||
* @param \Domain\Models\ValueObjects\Recipes\Inspiration[] $inspirations
|
|
||||||
* @param \Domain\Models\ValueObjects\Recipes\Season[] $seasons
|
|
||||||
* @return LengthAwarePaginator<array-key,Recipe>
|
|
||||||
*/
|
|
||||||
public function getPaginatedRecipes(
|
|
||||||
Pagination $pagination,
|
|
||||||
array $productRanges = [],
|
|
||||||
array $consumptionTimes = [],
|
|
||||||
array $inspirations = [],
|
|
||||||
array $seasons = [],
|
|
||||||
string $search = ''
|
|
||||||
): LengthAwarePaginator;
|
|
||||||
|
|
||||||
/** @return \Domain\Models\Entities\Recipe[] */
|
|
||||||
public function getRelatedRecipes(ProductId $productId, int $limit = 4): array;
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Contracts;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template TEntity
|
|
||||||
* @template TId
|
|
||||||
*
|
|
||||||
* @method TEntity|null find(TId $id)
|
|
||||||
*/
|
|
||||||
interface Directory
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Contracts;
|
|
||||||
|
|
||||||
interface Executor
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @template TInputAction of \App\Contracts\ActionInput
|
|
||||||
* @template TInputQuery of \App\Contracts\QueryInput
|
|
||||||
* @template TResult
|
|
||||||
*
|
|
||||||
* @param \App\Contracts\Query<TInputQuery,TResult>|\App\Contracts\Action<TInputAction,TResult> $executable
|
|
||||||
* @param TInputAction|TInputQuery $input
|
|
||||||
* @return TResult
|
|
||||||
*/
|
|
||||||
public function execute(Action | Query $executable, ActionInput | QueryInput $input): mixed;
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Contracts;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template TInput of \App\Contracts\QueryInput
|
|
||||||
* @template TResult
|
|
||||||
*/
|
|
||||||
interface Query
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param TInput $input
|
|
||||||
* @return TResult
|
|
||||||
*/
|
|
||||||
public function __invoke(QueryInput $input);
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Contracts;
|
|
||||||
|
|
||||||
interface QueryInput
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Contracts;
|
|
||||||
|
|
||||||
final class QueryInputEmpty implements QueryInput
|
|
||||||
{
|
|
||||||
public static function create(): self
|
|
||||||
{
|
|
||||||
return new self();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Contracts;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template TModel of \Illuminate\Database\Eloquent\Model
|
|
||||||
* @template TId of \Ramsey\Uuid\UuidInterface|int
|
|
||||||
*
|
|
||||||
* @method TModel|null find(TId $id)
|
|
||||||
* @method void persist(TModel $model)
|
|
||||||
* @method void delete(TModel $model)
|
|
||||||
*/
|
|
||||||
interface Repository
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\Create;
|
|
||||||
|
|
||||||
use App\Contracts\Action;
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use App\Models\Product;
|
|
||||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
|
||||||
use Ramsey\Uuid\Uuid;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* phpcs:disable Generic.Files.LineLength.TooLong
|
|
||||||
*
|
|
||||||
* @implements \App\Contracts\Action<\App\Cqs\Actions\Products\Create\CreateProductInput, \Domain\Models\ValueObjects\Entities\ProductId|\App\Cqs\Actions\Products\Create\FailedToCreateProduct>
|
|
||||||
* phpcs:enable
|
|
||||||
*/
|
|
||||||
final class CreateProduct implements Action
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param \App\Cqs\Actions\Products\Create\CreateProductInput $input
|
|
||||||
*/
|
|
||||||
public function __invoke(ActionInput $input): ProductId | FailedToCreateProduct
|
|
||||||
{
|
|
||||||
Assert::isInstanceOf($input, CreateProductInput::class);
|
|
||||||
|
|
||||||
$product = new Product([
|
|
||||||
'seo_title' => $input->seoTitle,
|
|
||||||
'seo_description' => $input->seoDescription,
|
|
||||||
'name' => $input->name,
|
|
||||||
'picture' => $input->picture,
|
|
||||||
'picture_alt' => $input->pictureAlt,
|
|
||||||
'picture_title' => $input->pictureTitle,
|
|
||||||
'category' => $input->category,
|
|
||||||
'technology' => $input->technology,
|
|
||||||
'is_egalim' => $input->isEgalim,
|
|
||||||
'is_published' => $input->isPublished,
|
|
||||||
'breeding_methods' => $input->breedingMethods,
|
|
||||||
'businesses' => $input->businesses,
|
|
||||||
'formats' => $input->formats,
|
|
||||||
'description' => $input->description,
|
|
||||||
'details_and_advantages' => $input->detailsAndAdvantages,
|
|
||||||
'handling_advices' => $input->handlingAdvices,
|
|
||||||
'ingredients' => $input->ingredients,
|
|
||||||
'conservation_before_opening' => $input->conservationBeforeOpening,
|
|
||||||
'conservation_after_opening' => $input->conservationAfterOpening,
|
|
||||||
'nutritional_values' => $input->nutritionalValues,
|
|
||||||
'pdf' => $input->pdf,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (! $product->save()) {
|
|
||||||
return new FailedToCreateProduct();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ProductId(Uuid::fromString($product->id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\Create;
|
|
||||||
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use Domain\Models\ValueObjects\Products\BreedingMethod;
|
|
||||||
use Domain\Models\ValueObjects\Products\Business;
|
|
||||||
use Domain\Models\ValueObjects\Products\NutritionnalUnit;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductFormat;
|
|
||||||
use Domain\Models\ValueObjects\Products\Technology;
|
|
||||||
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
final readonly class CreateProductInput implements ActionInput
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param BreedingMethod[] $breedingMethods
|
|
||||||
* @param Business[] $businesses
|
|
||||||
* @param ProductFormat[] $formats
|
|
||||||
* @param array<string, array{unit: NutritionnalUnit, value: string}> $nutritionalValues
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
public string $name,
|
|
||||||
public string $seoTitle,
|
|
||||||
public string $seoDescription,
|
|
||||||
public ProductCategory $category,
|
|
||||||
public Technology $technology,
|
|
||||||
public bool $isEgalim,
|
|
||||||
public bool $isPublished,
|
|
||||||
public array $breedingMethods,
|
|
||||||
public array $businesses,
|
|
||||||
public array $formats,
|
|
||||||
public string $description,
|
|
||||||
public string $detailsAndAdvantages,
|
|
||||||
public string $handlingAdvices,
|
|
||||||
public ?string $ingredients,
|
|
||||||
public string $conservationBeforeOpening,
|
|
||||||
public ?string $conservationAfterOpening,
|
|
||||||
public ?StorageFilePath $picture,
|
|
||||||
public string $pictureAlt,
|
|
||||||
public string $pictureTitle,
|
|
||||||
public ?StorageFilePath $pdf,
|
|
||||||
public array $nutritionalValues,
|
|
||||||
) {
|
|
||||||
Assert::notEmpty($this->name);
|
|
||||||
Assert::notEmpty($this->seoTitle);
|
|
||||||
Assert::notEmpty($this->seoDescription);
|
|
||||||
Assert::notEmpty($this->description);
|
|
||||||
Assert::notEmpty($this->detailsAndAdvantages);
|
|
||||||
Assert::notEmpty($this->handlingAdvices);
|
|
||||||
Assert::notEmpty($this->conservationBeforeOpening);
|
|
||||||
Assert::notEmpty($this->pictureAlt);
|
|
||||||
Assert::notEmpty($this->pictureTitle);
|
|
||||||
|
|
||||||
Assert::allIsAOf($this->breedingMethods, BreedingMethod::class);
|
|
||||||
Assert::allIsAOf($this->businesses, Business::class);
|
|
||||||
Assert::allIsAOf($this->formats, ProductFormat::class);
|
|
||||||
|
|
||||||
Assert::notEmpty($this->breedingMethods);
|
|
||||||
Assert::notEmpty($this->businesses);
|
|
||||||
Assert::notEmpty($this->formats);
|
|
||||||
|
|
||||||
when($this->ingredients !== null, fn () => Assert::notEmpty($this->ingredients));
|
|
||||||
when($this->conservationAfterOpening !== null, fn () => Assert::notEmpty($this->conservationAfterOpening));
|
|
||||||
|
|
||||||
foreach ($this->nutritionalValues as $value) {
|
|
||||||
Assert::isArray($value);
|
|
||||||
Assert::keyExists($value, 'unit');
|
|
||||||
Assert::keyExists($value, 'value');
|
|
||||||
Assert::isInstanceOf($value['unit'], NutritionnalUnit::class);
|
|
||||||
Assert::string($value['value']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\Create;
|
|
||||||
|
|
||||||
final readonly class FailedToCreateProduct
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\Delete;
|
|
||||||
|
|
||||||
use App\Contracts\Action;
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use App\Models\Product;
|
|
||||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
|
||||||
use Ramsey\Uuid\Uuid;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* phpcs:disable Generic.Files.LineLength.TooLong
|
|
||||||
*
|
|
||||||
* @implements \App\Contracts\Action<\App\Cqs\Actions\Products\Delete\DeleteProductInput, \Domain\Models\ValueObjects\Entities\ProductId|\App\Cqs\Actions\Products\Delete\FailedToDeleteProduct>
|
|
||||||
* phpcs:enable
|
|
||||||
*/
|
|
||||||
final class DeleteProduct implements Action
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param \App\Cqs\Actions\Products\Delete\DeleteProductInput $input
|
|
||||||
*/
|
|
||||||
public function __invoke(ActionInput $input): ProductId | FailedToDeleteProduct
|
|
||||||
{
|
|
||||||
Assert::isInstanceOf($input, DeleteProductInput::class);
|
|
||||||
|
|
||||||
$product = Product::query()->findOrFail((string) $input->id->value);
|
|
||||||
|
|
||||||
if (! $product->delete()) {
|
|
||||||
return new FailedToDeleteProduct();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ProductId(Uuid::fromString($product->id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\Delete;
|
|
||||||
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
|
||||||
|
|
||||||
final readonly class DeleteProductInput implements ActionInput
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public ProductId $id,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\Delete;
|
|
||||||
|
|
||||||
final readonly class FailedToDeleteProduct
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\Duplicate;
|
|
||||||
|
|
||||||
use App\Contracts\Action;
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use App\Lib\PublicS3Disk;
|
|
||||||
use App\Models\Product;
|
|
||||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Ramsey\Uuid\Uuid;
|
|
||||||
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* phpcs:disable Generic.Files.LineLength.TooLong
|
|
||||||
*
|
|
||||||
* @implements \App\Contracts\Action<\App\Cqs\Actions\Products\Duplicate\DuplicateProductInput, \Domain\Models\ValueObjects\Entities\ProductId|\App\Cqs\Actions\Products\Duplicate\FailedToDuplicateProduct>
|
|
||||||
* phpcs:enable
|
|
||||||
*/
|
|
||||||
final class DuplicateProduct implements Action
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly PublicS3Disk $s3Disk,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \App\Cqs\Actions\Products\Duplicate\DuplicateProductInput $input
|
|
||||||
*/
|
|
||||||
public function __invoke(ActionInput $input): ProductId | FailedToDuplicateProduct
|
|
||||||
{
|
|
||||||
Assert::isInstanceOf($input, DuplicateProductInput::class);
|
|
||||||
|
|
||||||
$original = Product::withTrashed()->findOrFail((string) $input->productId->value);
|
|
||||||
|
|
||||||
$clone = $original->replicate();
|
|
||||||
$clone->name = $this->generateUniqueName($original->name);
|
|
||||||
$clone->slug = Str::slug($clone->name);
|
|
||||||
|
|
||||||
$duplicatedPicture = $this->duplicateFile($original->picture);
|
|
||||||
if ($duplicatedPicture !== null) {
|
|
||||||
$clone->picture = $duplicatedPicture;
|
|
||||||
}
|
|
||||||
$clone->pdf = $this->duplicateFile($original->pdf);
|
|
||||||
$clone->is_published = false;
|
|
||||||
|
|
||||||
if (! $clone->save()) {
|
|
||||||
return new FailedToDuplicateProduct();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ProductId(Uuid::fromString($clone->id));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function generateUniqueName(string $originalName): string
|
|
||||||
{
|
|
||||||
$rootName = $originalName;
|
|
||||||
if (preg_match('/^(.*?)(?: \(Copie(?: (\d+))?\))?$/', $originalName, $matches)) {
|
|
||||||
$rootName = $matches[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
$baseName = "{$rootName} (Copie)";
|
|
||||||
if (
|
|
||||||
! Product::withTrashed()
|
|
||||||
->where('name', $baseName)
|
|
||||||
->orWhere('slug', Str::slug($baseName))
|
|
||||||
->exists()
|
|
||||||
) {
|
|
||||||
return $baseName;
|
|
||||||
}
|
|
||||||
|
|
||||||
$i = 2;
|
|
||||||
while (
|
|
||||||
Product::withTrashed()
|
|
||||||
->where('name', "{$rootName} (Copie {$i})")
|
|
||||||
->orWhere('slug', Str::slug("{$rootName} (Copie {$i})"))
|
|
||||||
->exists()
|
|
||||||
) {
|
|
||||||
$i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "{$rootName} (Copie {$i})";
|
|
||||||
}
|
|
||||||
|
|
||||||
private function duplicateFile(?StorageFilePath $filePath): ?StorageFilePath
|
|
||||||
{
|
|
||||||
if ($filePath === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sourcePath = $filePath->value;
|
|
||||||
$disk = Storage::disk($this->s3Disk->disk->value);
|
|
||||||
|
|
||||||
if (! $disk->exists($sourcePath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$extension = pathinfo($sourcePath, PATHINFO_EXTENSION);
|
|
||||||
$dirname = pathinfo($sourcePath, PATHINFO_DIRNAME);
|
|
||||||
$newFilename = Str::uuid()->toString() . ($extension !== '' ? '.' . $extension : '');
|
|
||||||
$destinationPath = ($dirname !== '.' && $dirname !== '') ? "{$dirname}/{$newFilename}" : $newFilename;
|
|
||||||
|
|
||||||
if (! $disk->copy($sourcePath, $destinationPath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new StorageFilePath($destinationPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\Duplicate;
|
|
||||||
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
|
||||||
|
|
||||||
final readonly class DuplicateProductInput implements ActionInput
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public ProductId $productId,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\Duplicate;
|
|
||||||
|
|
||||||
final readonly class FailedToDuplicateProduct
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\ForceDelete;
|
|
||||||
|
|
||||||
final readonly class FailedToForceDeleteProduct
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\ForceDelete;
|
|
||||||
|
|
||||||
use App\Contracts\Action;
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use App\Lib\PublicS3Disk;
|
|
||||||
use App\Models\Product;
|
|
||||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
|
||||||
use Ramsey\Uuid\Uuid;
|
|
||||||
use Uwil\Storage\Contracts\StorageService;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* phpcs:disable Generic.Files.LineLength.TooLong
|
|
||||||
*
|
|
||||||
* @implements \App\Contracts\Action<\App\Cqs\Actions\Products\ForceDelete\ForceDeleteProductInput, \Domain\Models\ValueObjects\Entities\ProductId|\App\Cqs\Actions\Products\ForceDelete\FailedToForceDeleteProduct>
|
|
||||||
* phpcs:enable
|
|
||||||
*/
|
|
||||||
final class ForceDeleteProduct implements Action
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly StorageService $storageService,
|
|
||||||
private readonly PublicS3Disk $s3Disk,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \App\Cqs\Actions\Products\ForceDelete\ForceDeleteProductInput $input
|
|
||||||
*/
|
|
||||||
public function __invoke(ActionInput $input): ProductId | FailedToForceDeleteProduct
|
|
||||||
{
|
|
||||||
Assert::isInstanceOf($input, ForceDeleteProductInput::class);
|
|
||||||
|
|
||||||
$product = Product::withTrashed()->findOrFail((string) $input->id->value);
|
|
||||||
$picture = $product->picture;
|
|
||||||
$pdf = $product->pdf;
|
|
||||||
|
|
||||||
if (! $product->forceDelete()) {
|
|
||||||
return new FailedToForceDeleteProduct();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($picture !== null) {
|
|
||||||
$this->storageService->removeFromDisk($picture, $this->s3Disk->disk);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($pdf !== null) {
|
|
||||||
$this->storageService->removeFromDisk($pdf, $this->s3Disk->disk);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ProductId(Uuid::fromString($product->id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\ForceDelete;
|
|
||||||
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
|
||||||
|
|
||||||
final readonly class ForceDeleteProductInput implements ActionInput
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public ProductId $id,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\Restore;
|
|
||||||
|
|
||||||
final readonly class FailedToRestoreProduct
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\Restore;
|
|
||||||
|
|
||||||
use App\Contracts\Action;
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use App\Models\Product;
|
|
||||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
|
||||||
use Ramsey\Uuid\Uuid;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* phpcs:disable Generic.Files.LineLength.TooLong
|
|
||||||
*
|
|
||||||
* @implements \App\Contracts\Action<\App\Cqs\Actions\Products\Restore\RestoreProductInput, \Domain\Models\ValueObjects\Entities\ProductId|\App\Cqs\Actions\Products\Restore\FailedToRestoreProduct>
|
|
||||||
* phpcs:enable
|
|
||||||
*/
|
|
||||||
final class RestoreProduct implements Action
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param \App\Cqs\Actions\Products\Restore\RestoreProductInput $input
|
|
||||||
*/
|
|
||||||
public function __invoke(ActionInput $input): ProductId | FailedToRestoreProduct
|
|
||||||
{
|
|
||||||
Assert::isInstanceOf($input, RestoreProductInput::class);
|
|
||||||
|
|
||||||
$product = Product::withTrashed()->findOrFail((string) $input->id->value);
|
|
||||||
|
|
||||||
if (! $product->restore()) {
|
|
||||||
return new FailedToRestoreProduct();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ProductId(Uuid::fromString($product->id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\Restore;
|
|
||||||
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
|
||||||
|
|
||||||
final readonly class RestoreProductInput implements ActionInput
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public ProductId $id,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\Update;
|
|
||||||
|
|
||||||
final readonly class FailedToUpdateProduct
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\Update;
|
|
||||||
|
|
||||||
use App\Contracts\Action;
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use App\Dto\ProductDto;
|
|
||||||
use App\Lib\PublicS3Disk;
|
|
||||||
use App\Models\Product;
|
|
||||||
use Uwil\Storage\Contracts\StorageService;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* phpcs:disable Generic.Files.LineLength.TooLong
|
|
||||||
*
|
|
||||||
* @implements \App\Contracts\Action<\App\Cqs\Actions\Products\Update\UpdateProductInput, \App\Dto\ProductDto|\App\Cqs\Actions\Products\Update\FailedToUpdateProduct>
|
|
||||||
* phpcs:enable
|
|
||||||
*/
|
|
||||||
final class UpdateProduct implements Action
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly StorageService $storageService,
|
|
||||||
private readonly PublicS3Disk $s3Disk,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \App\Cqs\Actions\Products\Update\UpdateProductInput $input
|
|
||||||
*/
|
|
||||||
public function __invoke(ActionInput $input): ProductDto | FailedToUpdateProduct
|
|
||||||
{
|
|
||||||
Assert::isInstanceOf($input, UpdateProductInput::class);
|
|
||||||
|
|
||||||
$product = Product::query()->findOrFail((string) $input->id->value);
|
|
||||||
|
|
||||||
$oldPicture = $product->picture;
|
|
||||||
$oldPdf = $product->pdf;
|
|
||||||
|
|
||||||
$product->fill([
|
|
||||||
'seo_title' => $input->seoTitle,
|
|
||||||
'seo_description' => $input->seoDescription,
|
|
||||||
'name' => $input->name,
|
|
||||||
'picture_alt' => $input->pictureAlt,
|
|
||||||
'picture_title' => $input->pictureTitle,
|
|
||||||
'category' => $input->category,
|
|
||||||
'technology' => $input->technology,
|
|
||||||
'is_egalim' => $input->isEgalim,
|
|
||||||
'is_published' => $input->isPublished,
|
|
||||||
'breeding_methods' => $input->breedingMethods,
|
|
||||||
'businesses' => $input->businesses,
|
|
||||||
'formats' => $input->formats,
|
|
||||||
'description' => $input->description,
|
|
||||||
'details_and_advantages' => $input->detailsAndAdvantages,
|
|
||||||
'handling_advices' => $input->handlingAdvices,
|
|
||||||
'ingredients' => $input->ingredients,
|
|
||||||
'conservation_before_opening' => $input->conservationBeforeOpening,
|
|
||||||
'conservation_after_opening' => $input->conservationAfterOpening,
|
|
||||||
'nutritional_values' => $input->nutritionalValues,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($input->picture !== null) {
|
|
||||||
$product->picture = $input->picture;
|
|
||||||
}
|
|
||||||
|
|
||||||
$product->pdf = $input->pdf;
|
|
||||||
|
|
||||||
if (! $product->save()) {
|
|
||||||
return new FailedToUpdateProduct();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up replaced or removed S3 files
|
|
||||||
if ($input->picture !== null && $oldPicture !== null && $oldPicture->value !== $input->picture->value) {
|
|
||||||
$this->storageService->removeFromDisk($oldPicture, $this->s3Disk->disk);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($oldPdf !== null && ($input->pdf === null || $oldPdf->value !== $input->pdf->value)) {
|
|
||||||
$this->storageService->removeFromDisk($oldPdf, $this->s3Disk->disk);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ProductDto::fromModel($product, $this->s3Disk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Products\Update;
|
|
||||||
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
|
||||||
use Domain\Models\ValueObjects\Products\BreedingMethod;
|
|
||||||
use Domain\Models\ValueObjects\Products\Business;
|
|
||||||
use Domain\Models\ValueObjects\Products\NutritionnalUnit;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductFormat;
|
|
||||||
use Domain\Models\ValueObjects\Products\Technology;
|
|
||||||
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
final readonly class UpdateProductInput implements ActionInput
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param BreedingMethod[] $breedingMethods
|
|
||||||
* @param Business[] $businesses
|
|
||||||
* @param ProductFormat[] $formats
|
|
||||||
* @param array<string, array{unit: NutritionnalUnit, value: string}> $nutritionalValues
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
public ProductId $id,
|
|
||||||
public string $name,
|
|
||||||
public string $seoTitle,
|
|
||||||
public string $seoDescription,
|
|
||||||
public ProductCategory $category,
|
|
||||||
public Technology $technology,
|
|
||||||
public bool $isEgalim,
|
|
||||||
public bool $isPublished,
|
|
||||||
public array $breedingMethods,
|
|
||||||
public array $businesses,
|
|
||||||
public array $formats,
|
|
||||||
public string $description,
|
|
||||||
public string $detailsAndAdvantages,
|
|
||||||
public string $handlingAdvices,
|
|
||||||
public ?string $ingredients,
|
|
||||||
public string $conservationBeforeOpening,
|
|
||||||
public ?string $conservationAfterOpening,
|
|
||||||
public ?StorageFilePath $picture,
|
|
||||||
public string $pictureAlt,
|
|
||||||
public string $pictureTitle,
|
|
||||||
public ?StorageFilePath $pdf,
|
|
||||||
public array $nutritionalValues,
|
|
||||||
) {
|
|
||||||
Assert::notEmpty($this->name);
|
|
||||||
Assert::notEmpty($this->seoTitle);
|
|
||||||
Assert::notEmpty($this->seoDescription);
|
|
||||||
Assert::notEmpty($this->description);
|
|
||||||
Assert::notEmpty($this->detailsAndAdvantages);
|
|
||||||
Assert::notEmpty($this->handlingAdvices);
|
|
||||||
Assert::notEmpty($this->conservationBeforeOpening);
|
|
||||||
Assert::notEmpty($this->pictureAlt);
|
|
||||||
Assert::notEmpty($this->pictureTitle);
|
|
||||||
|
|
||||||
Assert::allIsAOf($this->breedingMethods, BreedingMethod::class);
|
|
||||||
Assert::allIsAOf($this->businesses, Business::class);
|
|
||||||
Assert::allIsAOf($this->formats, ProductFormat::class);
|
|
||||||
|
|
||||||
Assert::notEmpty($this->breedingMethods);
|
|
||||||
Assert::notEmpty($this->businesses);
|
|
||||||
Assert::notEmpty($this->formats);
|
|
||||||
|
|
||||||
when($this->ingredients !== null, fn () => Assert::notEmpty($this->ingredients));
|
|
||||||
when($this->conservationAfterOpening !== null, fn () => Assert::notEmpty($this->conservationAfterOpening));
|
|
||||||
|
|
||||||
foreach ($this->nutritionalValues as $value) {
|
|
||||||
Assert::isArray($value);
|
|
||||||
Assert::keyExists($value, 'unit');
|
|
||||||
Assert::keyExists($value, 'value');
|
|
||||||
Assert::isInstanceOf($value['unit'], NutritionnalUnit::class);
|
|
||||||
Assert::string($value['value']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\Create;
|
|
||||||
|
|
||||||
use App\Contracts\Action;
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use App\Models\Recipe;
|
|
||||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
|
||||||
use Ramsey\Uuid\Uuid;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* phpcs:disable Generic.Files.LineLength.TooLong
|
|
||||||
*
|
|
||||||
* @implements \App\Contracts\Action<\App\Cqs\Actions\Recipes\Create\CreateRecipeInput, \Domain\Models\ValueObjects\Entities\RecipeId|\App\Cqs\Actions\Recipes\Create\FailedToCreateRecipe>
|
|
||||||
* phpcs:enable
|
|
||||||
*/
|
|
||||||
final class CreateRecipe implements Action
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param \App\Cqs\Actions\Recipes\Create\CreateRecipeInput $input
|
|
||||||
*/
|
|
||||||
public function __invoke(ActionInput $input): RecipeId | FailedToCreateRecipe
|
|
||||||
{
|
|
||||||
Assert::isInstanceOf($input, CreateRecipeInput::class);
|
|
||||||
|
|
||||||
$recipe = new Recipe([
|
|
||||||
'seo_title' => $input->seoTitle,
|
|
||||||
'seo_description' => $input->seoDescription,
|
|
||||||
'name' => $input->name,
|
|
||||||
'is_published' => $input->isPublished,
|
|
||||||
'picture' => $input->picture,
|
|
||||||
'picture_alt' => $input->pictureAlt,
|
|
||||||
'picture_title' => $input->pictureTitle,
|
|
||||||
'product_category' => $input->productCategory,
|
|
||||||
'consomption_time' => $input->consomptionTime,
|
|
||||||
'inspiration' => $input->inspiration,
|
|
||||||
'season' => $input->season,
|
|
||||||
'description' => $input->description,
|
|
||||||
'details' => $input->details,
|
|
||||||
'ingredients_list' => $input->ingredientsList,
|
|
||||||
'preparation_steps' => $input->preparationSteps,
|
|
||||||
'tips_and_advices' => $input->tipsAndAdvices,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (! $recipe->save()) {
|
|
||||||
return new FailedToCreateRecipe();
|
|
||||||
}
|
|
||||||
|
|
||||||
$recipe->products()->sync($input->products);
|
|
||||||
|
|
||||||
return new RecipeId(Uuid::fromString($recipe->id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\Create;
|
|
||||||
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
|
||||||
use Domain\Models\ValueObjects\Recipes\ConsumptionTime;
|
|
||||||
use Domain\Models\ValueObjects\Recipes\Inspiration;
|
|
||||||
use Domain\Models\ValueObjects\Recipes\Season;
|
|
||||||
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
final readonly class CreateRecipeInput implements ActionInput
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param string[] $products
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
public string $name,
|
|
||||||
public string $seoTitle,
|
|
||||||
public string $seoDescription,
|
|
||||||
public bool $isPublished,
|
|
||||||
public ProductCategory $productCategory,
|
|
||||||
public ConsumptionTime $consomptionTime,
|
|
||||||
public Inspiration $inspiration,
|
|
||||||
public Season $season,
|
|
||||||
public ?string $description,
|
|
||||||
public ?string $details,
|
|
||||||
public string $ingredientsList,
|
|
||||||
public string $preparationSteps,
|
|
||||||
public ?string $tipsAndAdvices,
|
|
||||||
public ?StorageFilePath $picture,
|
|
||||||
public string $pictureAlt,
|
|
||||||
public string $pictureTitle,
|
|
||||||
public array $products = [],
|
|
||||||
) {
|
|
||||||
Assert::notEmpty($this->name);
|
|
||||||
Assert::notEmpty($this->seoTitle);
|
|
||||||
Assert::notEmpty($this->seoDescription);
|
|
||||||
Assert::notEmpty($this->ingredientsList);
|
|
||||||
Assert::notEmpty($this->preparationSteps);
|
|
||||||
Assert::notEmpty($this->pictureAlt);
|
|
||||||
Assert::notEmpty($this->pictureTitle);
|
|
||||||
|
|
||||||
when($this->description !== null, fn () => Assert::notEmpty($this->description));
|
|
||||||
when($this->details !== null, fn () => Assert::notEmpty($this->details));
|
|
||||||
when($this->tipsAndAdvices !== null, fn () => Assert::notEmpty($this->tipsAndAdvices));
|
|
||||||
|
|
||||||
Assert::allStringNotEmpty($this->products);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\Create;
|
|
||||||
|
|
||||||
final readonly class FailedToCreateRecipe
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\Delete;
|
|
||||||
|
|
||||||
use App\Contracts\Action;
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use App\Models\Recipe;
|
|
||||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
|
||||||
use Ramsey\Uuid\Uuid;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* phpcs:disable Generic.Files.LineLength.TooLong
|
|
||||||
*
|
|
||||||
* @implements \App\Contracts\Action<\App\Cqs\Actions\Recipes\Delete\DeleteRecipeInput, \Domain\Models\ValueObjects\Entities\RecipeId|\App\Cqs\Actions\Recipes\Delete\FailedToDeleteRecipe>
|
|
||||||
* phpcs:enable
|
|
||||||
*/
|
|
||||||
final class DeleteRecipe implements Action
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param \App\Cqs\Actions\Recipes\Delete\DeleteRecipeInput $input
|
|
||||||
*/
|
|
||||||
public function __invoke(ActionInput $input): RecipeId | FailedToDeleteRecipe
|
|
||||||
{
|
|
||||||
Assert::isInstanceOf($input, DeleteRecipeInput::class);
|
|
||||||
|
|
||||||
$recipe = Recipe::query()->findOrFail((string) $input->id->value);
|
|
||||||
|
|
||||||
if (! $recipe->delete()) {
|
|
||||||
return new FailedToDeleteRecipe();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RecipeId(Uuid::fromString($recipe->id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\Delete;
|
|
||||||
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
|
||||||
|
|
||||||
final readonly class DeleteRecipeInput implements ActionInput
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public RecipeId $id,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\Delete;
|
|
||||||
|
|
||||||
final readonly class FailedToDeleteRecipe
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\Duplicate;
|
|
||||||
|
|
||||||
use App\Contracts\Action;
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use App\Lib\PublicS3Disk;
|
|
||||||
use App\Models\Recipe;
|
|
||||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Ramsey\Uuid\Uuid;
|
|
||||||
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* phpcs:disable Generic.Files.LineLength.TooLong
|
|
||||||
*
|
|
||||||
* @implements \App\Contracts\Action<\App\Cqs\Actions\Recipes\Duplicate\DuplicateRecipeInput, \Domain\Models\ValueObjects\Entities\RecipeId|\App\Cqs\Actions\Recipes\Duplicate\FailedToDuplicateRecipe>
|
|
||||||
* phpcs:enable
|
|
||||||
*/
|
|
||||||
final class DuplicateRecipe implements Action
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly PublicS3Disk $s3Disk,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \App\Cqs\Actions\Recipes\Duplicate\DuplicateRecipeInput $input
|
|
||||||
*/
|
|
||||||
public function __invoke(ActionInput $input): RecipeId | FailedToDuplicateRecipe
|
|
||||||
{
|
|
||||||
Assert::isInstanceOf($input, DuplicateRecipeInput::class);
|
|
||||||
|
|
||||||
$original = Recipe::withTrashed()->with('products')->findOrFail((string) $input->recipeId->value);
|
|
||||||
|
|
||||||
$clone = $original->replicate();
|
|
||||||
$clone->name = $this->generateUniqueName($original->name);
|
|
||||||
$clone->slug = Str::slug($clone->name);
|
|
||||||
|
|
||||||
$duplicatedPicture = $this->duplicateFile($original->picture);
|
|
||||||
if ($duplicatedPicture !== null) {
|
|
||||||
$clone->picture = $duplicatedPicture;
|
|
||||||
}
|
|
||||||
|
|
||||||
$clone->is_published = false;
|
|
||||||
|
|
||||||
if (! $clone->save()) {
|
|
||||||
return new FailedToDuplicateRecipe();
|
|
||||||
}
|
|
||||||
|
|
||||||
$productIds = $original->products->pluck('id')->all();
|
|
||||||
if (! empty($productIds)) {
|
|
||||||
$clone->products()->sync($productIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RecipeId(Uuid::fromString($clone->id));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function generateUniqueName(string $originalName): string
|
|
||||||
{
|
|
||||||
$rootName = $originalName;
|
|
||||||
if (preg_match('/^(.*?)(?: \(Copie(?: (\d+))?\))?$/', $originalName, $matches)) {
|
|
||||||
$rootName = $matches[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
$baseName = "{$rootName} (Copie)";
|
|
||||||
if (
|
|
||||||
! Recipe::withTrashed()
|
|
||||||
->where('name', $baseName)
|
|
||||||
->orWhere('slug', Str::slug($baseName))
|
|
||||||
->exists()
|
|
||||||
) {
|
|
||||||
return $baseName;
|
|
||||||
}
|
|
||||||
|
|
||||||
$i = 2;
|
|
||||||
while (
|
|
||||||
Recipe::withTrashed()
|
|
||||||
->where('name', "{$rootName} (Copie {$i})")
|
|
||||||
->orWhere('slug', Str::slug("{$rootName} (Copie {$i})"))->exists()
|
|
||||||
) {
|
|
||||||
$i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "{$rootName} (Copie {$i})";
|
|
||||||
}
|
|
||||||
|
|
||||||
private function duplicateFile(?StorageFilePath $filePath): ?StorageFilePath
|
|
||||||
{
|
|
||||||
if ($filePath === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sourcePath = $filePath->value;
|
|
||||||
$disk = Storage::disk($this->s3Disk->disk->value);
|
|
||||||
|
|
||||||
if (! $disk->exists($sourcePath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$extension = pathinfo($sourcePath, PATHINFO_EXTENSION);
|
|
||||||
$dirname = pathinfo($sourcePath, PATHINFO_DIRNAME);
|
|
||||||
$newFilename = Str::uuid()->toString() . ($extension !== '' ? '.' . $extension : '');
|
|
||||||
$destinationPath = ($dirname !== '.' && $dirname !== '') ? "{$dirname}/{$newFilename}" : $newFilename;
|
|
||||||
|
|
||||||
if (! $disk->copy($sourcePath, $destinationPath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new StorageFilePath($destinationPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\Duplicate;
|
|
||||||
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
|
||||||
|
|
||||||
final readonly class DuplicateRecipeInput implements ActionInput
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public RecipeId $recipeId,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\Duplicate;
|
|
||||||
|
|
||||||
final readonly class FailedToDuplicateRecipe
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\ForceDelete;
|
|
||||||
|
|
||||||
final readonly class FailedToForceDeleteRecipe
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\ForceDelete;
|
|
||||||
|
|
||||||
use App\Contracts\Action;
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use App\Lib\PublicS3Disk;
|
|
||||||
use App\Models\Recipe;
|
|
||||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
|
||||||
use Ramsey\Uuid\Uuid;
|
|
||||||
use Uwil\Storage\Contracts\StorageService;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* phpcs:disable Generic.Files.LineLength.TooLong
|
|
||||||
*
|
|
||||||
* @implements \App\Contracts\Action<\App\Cqs\Actions\Recipes\ForceDelete\ForceDeleteRecipeInput, \Domain\Models\ValueObjects\Entities\RecipeId|\App\Cqs\Actions\Recipes\ForceDelete\FailedToForceDeleteRecipe>
|
|
||||||
* phpcs:enable
|
|
||||||
*/
|
|
||||||
final class ForceDeleteRecipe implements Action
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly StorageService $storageService,
|
|
||||||
private readonly PublicS3Disk $s3Disk,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \App\Cqs\Actions\Recipes\ForceDelete\ForceDeleteRecipeInput $input
|
|
||||||
*/
|
|
||||||
public function __invoke(ActionInput $input): RecipeId | FailedToForceDeleteRecipe
|
|
||||||
{
|
|
||||||
Assert::isInstanceOf($input, ForceDeleteRecipeInput::class);
|
|
||||||
|
|
||||||
$recipe = Recipe::withTrashed()->findOrFail((string) $input->id->value);
|
|
||||||
$picture = $recipe->picture;
|
|
||||||
|
|
||||||
if (! $recipe->forceDelete()) {
|
|
||||||
return new FailedToForceDeleteRecipe();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($picture !== null) {
|
|
||||||
$this->storageService->removeFromDisk($picture, $this->s3Disk->disk);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RecipeId(Uuid::fromString($recipe->id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\ForceDelete;
|
|
||||||
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
|
||||||
|
|
||||||
final readonly class ForceDeleteRecipeInput implements ActionInput
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public RecipeId $id,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\Restore;
|
|
||||||
|
|
||||||
final readonly class FailedToRestoreRecipe
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\Restore;
|
|
||||||
|
|
||||||
use App\Contracts\Action;
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use App\Models\Recipe;
|
|
||||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
|
||||||
use Ramsey\Uuid\Uuid;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* phpcs:disable Generic.Files.LineLength.TooLong
|
|
||||||
*
|
|
||||||
* @implements \App\Contracts\Action<\App\Cqs\Actions\Recipes\Restore\RestoreRecipeInput, \Domain\Models\ValueObjects\Entities\RecipeId|\App\Cqs\Actions\Recipes\Restore\FailedToRestoreRecipe>
|
|
||||||
* phpcs:enable
|
|
||||||
*/
|
|
||||||
final class RestoreRecipe implements Action
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param \App\Cqs\Actions\Recipes\Restore\RestoreRecipeInput $input
|
|
||||||
*/
|
|
||||||
public function __invoke(ActionInput $input): RecipeId | FailedToRestoreRecipe
|
|
||||||
{
|
|
||||||
Assert::isInstanceOf($input, RestoreRecipeInput::class);
|
|
||||||
|
|
||||||
$recipe = Recipe::withTrashed()->findOrFail((string) $input->id->value);
|
|
||||||
|
|
||||||
if (! $recipe->restore()) {
|
|
||||||
return new FailedToRestoreRecipe();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RecipeId(Uuid::fromString($recipe->id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\Restore;
|
|
||||||
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
|
||||||
|
|
||||||
final readonly class RestoreRecipeInput implements ActionInput
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public RecipeId $id,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\Update;
|
|
||||||
|
|
||||||
final readonly class FailedToUpdateRecipe
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\Update;
|
|
||||||
|
|
||||||
use App\Contracts\Action;
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use App\Dto\RecipeDto;
|
|
||||||
use App\Lib\PublicS3Disk;
|
|
||||||
use App\Models\Recipe;
|
|
||||||
use Uwil\Storage\Contracts\StorageService;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* phpcs:disable Generic.Files.LineLength.TooLong
|
|
||||||
*
|
|
||||||
* @implements \App\Contracts\Action<\App\Cqs\Actions\Recipes\Update\UpdateRecipeInput, \App\Dto\RecipeDto|\App\Cqs\Actions\Recipes\Update\FailedToUpdateRecipe>
|
|
||||||
* phpcs:enable
|
|
||||||
*/
|
|
||||||
final class UpdateRecipe implements Action
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly StorageService $storageService,
|
|
||||||
private readonly PublicS3Disk $s3Disk,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \App\Cqs\Actions\Recipes\Update\UpdateRecipeInput $input
|
|
||||||
*/
|
|
||||||
public function __invoke(ActionInput $input): RecipeDto | FailedToUpdateRecipe
|
|
||||||
{
|
|
||||||
Assert::isInstanceOf($input, UpdateRecipeInput::class);
|
|
||||||
|
|
||||||
$recipe = Recipe::query()->findOrFail((string) $input->id->value);
|
|
||||||
|
|
||||||
$oldPicture = $recipe->picture;
|
|
||||||
|
|
||||||
$recipe->fill([
|
|
||||||
'seo_title' => $input->seoTitle,
|
|
||||||
'seo_description' => $input->seoDescription,
|
|
||||||
'name' => $input->name,
|
|
||||||
'is_published' => $input->isPublished,
|
|
||||||
'picture_alt' => $input->pictureAlt,
|
|
||||||
'picture_title' => $input->pictureTitle,
|
|
||||||
'product_category' => $input->productCategory,
|
|
||||||
'consomption_time' => $input->consomptionTime,
|
|
||||||
'inspiration' => $input->inspiration,
|
|
||||||
'season' => $input->season,
|
|
||||||
'description' => $input->description,
|
|
||||||
'details' => $input->details,
|
|
||||||
'ingredients_list' => $input->ingredientsList,
|
|
||||||
'preparation_steps' => $input->preparationSteps,
|
|
||||||
'tips_and_advices' => $input->tipsAndAdvices,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($input->picture !== null) {
|
|
||||||
$recipe->picture = $input->picture;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $recipe->save()) {
|
|
||||||
return new FailedToUpdateRecipe();
|
|
||||||
}
|
|
||||||
|
|
||||||
$recipe->products()->sync($input->products);
|
|
||||||
|
|
||||||
// Clean up replaced S3 picture
|
|
||||||
if ($input->picture !== null && $oldPicture !== null && $oldPicture->value !== $input->picture->value) {
|
|
||||||
$this->storageService->removeFromDisk($oldPicture, $this->s3Disk->disk);
|
|
||||||
}
|
|
||||||
|
|
||||||
return RecipeDto::fromModel($recipe, $this->s3Disk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Actions\Recipes\Update;
|
|
||||||
|
|
||||||
use App\Contracts\ActionInput;
|
|
||||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
|
||||||
use Domain\Models\ValueObjects\Recipes\ConsumptionTime;
|
|
||||||
use Domain\Models\ValueObjects\Recipes\Inspiration;
|
|
||||||
use Domain\Models\ValueObjects\Recipes\Season;
|
|
||||||
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
final readonly class UpdateRecipeInput implements ActionInput
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param string[] $products
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
public RecipeId $id,
|
|
||||||
public string $name,
|
|
||||||
public string $seoTitle,
|
|
||||||
public string $seoDescription,
|
|
||||||
public bool $isPublished,
|
|
||||||
public ProductCategory $productCategory,
|
|
||||||
public ConsumptionTime $consomptionTime,
|
|
||||||
public Inspiration $inspiration,
|
|
||||||
public Season $season,
|
|
||||||
public ?string $description,
|
|
||||||
public ?string $details,
|
|
||||||
public string $ingredientsList,
|
|
||||||
public string $preparationSteps,
|
|
||||||
public ?string $tipsAndAdvices,
|
|
||||||
public ?StorageFilePath $picture,
|
|
||||||
public string $pictureAlt,
|
|
||||||
public string $pictureTitle,
|
|
||||||
public array $products = [],
|
|
||||||
) {
|
|
||||||
Assert::notEmpty($this->name);
|
|
||||||
Assert::notEmpty($this->seoTitle);
|
|
||||||
Assert::notEmpty($this->seoDescription);
|
|
||||||
Assert::notEmpty($this->ingredientsList);
|
|
||||||
Assert::notEmpty($this->preparationSteps);
|
|
||||||
Assert::notEmpty($this->pictureAlt);
|
|
||||||
Assert::notEmpty($this->pictureTitle);
|
|
||||||
|
|
||||||
when($this->description !== null, fn () => Assert::notEmpty($this->description));
|
|
||||||
when($this->details !== null, fn () => Assert::notEmpty($this->details));
|
|
||||||
when($this->tipsAndAdvices !== null, fn () => Assert::notEmpty($this->tipsAndAdvices));
|
|
||||||
|
|
||||||
Assert::allStringNotEmpty($this->products);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Products;
|
|
||||||
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
use Ramsey\Uuid\UuidInterface;
|
|
||||||
|
|
||||||
final readonly class FindProductByIdInput implements QueryInput
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public UuidInterface $id,
|
|
||||||
public bool $includeTrashed = false
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Products;
|
|
||||||
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
|
|
||||||
final readonly class FindProductBySlugInput implements QueryInput
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public string $slug,
|
|
||||||
public bool $includeTrashed = false
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Products;
|
|
||||||
|
|
||||||
use App\Contracts\Query;
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
use App\Dto\ProductDto;
|
|
||||||
use App\Lib\PublicS3Disk;
|
|
||||||
use App\Models\Product;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @phpcs:disable Generic.Files.LineLength
|
|
||||||
*
|
|
||||||
* @implements \App\Contracts\Query<\App\Cqs\Queries\Products\FindProductByIdInput|\App\Cqs\Queries\Products\FindProductBySlugInput, \App\Dto\ProductDto|null>
|
|
||||||
*
|
|
||||||
* @phpcs:enable
|
|
||||||
*/
|
|
||||||
final readonly class FindProductQuery implements Query
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private PublicS3Disk $publicS3Disk,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
public function __invoke(QueryInput $input): ?ProductDto
|
|
||||||
{
|
|
||||||
Assert::true(
|
|
||||||
$input instanceof FindProductByIdInput || $input instanceof FindProductBySlugInput,
|
|
||||||
'Input must be an instance of FindProductByIdInput or FindProductBySlugInput.'
|
|
||||||
);
|
|
||||||
|
|
||||||
$query = Product::query();
|
|
||||||
|
|
||||||
if ($input->includeTrashed) {
|
|
||||||
$query->withTrashed();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var Product|null $product */
|
|
||||||
$product = match (true) {
|
|
||||||
$input instanceof FindProductByIdInput => $query->where('id', (string) $input->id)->first(),
|
|
||||||
$input instanceof FindProductBySlugInput => $query->where('slug', $input->slug)->first(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if ($product === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ProductDto::fromModel($product, $this->publicS3Disk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Products;
|
|
||||||
|
|
||||||
use App\Contracts\Query;
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
use App\Models\Product;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
|
||||||
use Domain\Models\ValueObjects\Products\Technology;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use SortDirection;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* phpcs:disable Generic.Files.LineLength
|
|
||||||
*
|
|
||||||
* @implements \App\Contracts\Query<\App\Cqs\Queries\Products\GetBoPaginatedProductsQueryInput, \App\Cqs\Queries\Products\GetBoPaginatedProductsQueryOutput>
|
|
||||||
* phpcs:enable
|
|
||||||
*/
|
|
||||||
final readonly class GetBoPaginatedProductsQuery implements Query
|
|
||||||
{
|
|
||||||
private const array SORTABLE_COLUMNS = [
|
|
||||||
'name',
|
|
||||||
'category',
|
|
||||||
'technology',
|
|
||||||
'is_egalim',
|
|
||||||
'is_published',
|
|
||||||
'created_at',
|
|
||||||
'updated_at',
|
|
||||||
];
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
public function __invoke(QueryInput $input): GetBoPaginatedProductsQueryOutput
|
|
||||||
{
|
|
||||||
Assert::isAOf($input, GetBoPaginatedProductsQueryInput::class);
|
|
||||||
|
|
||||||
/** @var Builder<Product> $query */
|
|
||||||
$query = Product::query();
|
|
||||||
|
|
||||||
if ($input->isTrash) {
|
|
||||||
$query->onlyTrashed();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($input->search !== null && $input->search !== '') {
|
|
||||||
$words = array_filter(explode(' ', $input->search));
|
|
||||||
$query->where(function (Builder $q) use ($words, $input): void {
|
|
||||||
foreach ($words as $word) {
|
|
||||||
$q->where('name', 'like', "%{$word}%");
|
|
||||||
}
|
|
||||||
$q->orWhere('name', 'like', "%{$input->search}%");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$sortCol = in_array($input->sortCol, self::SORTABLE_COLUMNS, true)
|
|
||||||
? $input->sortCol
|
|
||||||
: 'name';
|
|
||||||
|
|
||||||
$direction = $input->sortDirection === SortDirection::Descending ? 'desc' : 'asc';
|
|
||||||
$query->orderBy($sortCol, $direction);
|
|
||||||
|
|
||||||
$paginated = $query->paginate(
|
|
||||||
perPage: $input->pagination->pageSize(),
|
|
||||||
page: $input->pagination->pageNumber(),
|
|
||||||
)->withQueryString();
|
|
||||||
|
|
||||||
$searchFields = \implode(', ', [
|
|
||||||
trans('validation.attributes.name'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return new GetBoPaginatedProductsQueryOutput(
|
|
||||||
paginatedProducts: $paginated,
|
|
||||||
search: $input->search,
|
|
||||||
searchFields: $searchFields,
|
|
||||||
categories: ProductCategory::toArray(),
|
|
||||||
technologies: Technology::toArray(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Products;
|
|
||||||
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
use Domain\Models\ValueObjects\Pagination;
|
|
||||||
use SortDirection;
|
|
||||||
|
|
||||||
final readonly class GetBoPaginatedProductsQueryInput implements QueryInput
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public Pagination $pagination,
|
|
||||||
public ?string $search = null,
|
|
||||||
public string $sortCol = 'name',
|
|
||||||
public SortDirection $sortDirection = SortDirection::Ascending,
|
|
||||||
public bool $isTrash = false,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Products;
|
|
||||||
|
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
||||||
|
|
||||||
final readonly class GetBoPaginatedProductsQueryOutput
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param LengthAwarePaginator<int,\App\Models\Product> $paginatedProducts
|
|
||||||
* @param array<string, mixed> $categories
|
|
||||||
* @param array<string, mixed> $technologies
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
public LengthAwarePaginator $paginatedProducts,
|
|
||||||
public ?string $search,
|
|
||||||
public string $searchFields,
|
|
||||||
public array $categories,
|
|
||||||
public array $technologies,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Products;
|
|
||||||
|
|
||||||
use App\Contracts\Directories\ProductDirectory;
|
|
||||||
use App\Contracts\Query;
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @implements \App\Contracts\Query<\App\Cqs\Queries\Products\GetCategoriesProductsQueryInput, array<int, \App\Dto\CategoryProductsDto>>
|
|
||||||
*/
|
|
||||||
final readonly class GetCategoriesProductsQuery implements Query
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private ProductDirectory $productDirectory,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<int, \App\Dto\CategoryProductsDto>
|
|
||||||
*/
|
|
||||||
public function __invoke(QueryInput $input): array
|
|
||||||
{
|
|
||||||
Assert::isAOf($input, GetCategoriesProductsQueryInput::class);
|
|
||||||
|
|
||||||
$categories = $input->categories;
|
|
||||||
|
|
||||||
if (count($categories) === 0) {
|
|
||||||
$categories = ! $input->hasFilter ? [
|
|
||||||
ProductCategory::OMELETS_AND_DELI_AND_DESSERT_LINES,
|
|
||||||
ProductCategory::HARD_BOILED_EGGS,
|
|
||||||
] : ProductCategory::cases();
|
|
||||||
}
|
|
||||||
|
|
||||||
$categoriesProducts = [];
|
|
||||||
|
|
||||||
foreach ($categories as $category) {
|
|
||||||
$result = $this->productDirectory->getCategoryProducts(
|
|
||||||
category: $category,
|
|
||||||
breedingMethods: $input->breedingMethods,
|
|
||||||
technologies: $input->technologies,
|
|
||||||
businesses: $input->businesses,
|
|
||||||
isEgalim: $input->isEgalim,
|
|
||||||
search: $input->search,
|
|
||||||
limitPerCategory: -1
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($result !== null) {
|
|
||||||
$categoriesProducts[] = $result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $categoriesProducts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Products;
|
|
||||||
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
use Domain\Models\ValueObjects\Products\BreedingMethod;
|
|
||||||
use Domain\Models\ValueObjects\Products\Business;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
|
||||||
use Domain\Models\ValueObjects\Products\Technology;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
final readonly class GetCategoriesProductsQueryInput implements QueryInput
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param ProductCategory[] $categories
|
|
||||||
* @param BreedingMethod[] $breedingMethods
|
|
||||||
* @param Technology[] $technologies
|
|
||||||
* @param Business[] $businesses
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
public array $categories = [],
|
|
||||||
public array $breedingMethods = [],
|
|
||||||
public array $technologies = [],
|
|
||||||
public array $businesses = [],
|
|
||||||
public ?bool $isEgalim = null,
|
|
||||||
public string $search = '',
|
|
||||||
public bool $hasFilter = false,
|
|
||||||
) {
|
|
||||||
Assert::allIsAOf($this->categories, ProductCategory::class);
|
|
||||||
Assert::allIsAOf($this->breedingMethods, BreedingMethod::class);
|
|
||||||
Assert::allIsAOf($this->technologies, Technology::class);
|
|
||||||
Assert::allIsAOf($this->businesses, Business::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Products;
|
|
||||||
|
|
||||||
use App\Contracts\Directories\ProductDirectory;
|
|
||||||
use App\Contracts\Query;
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
use App\Contracts\QueryInputEmpty;
|
|
||||||
use App\Dto\ProductSummaryDto;
|
|
||||||
use App\Lib\PublicS3Disk;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductSummary;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @implements \App\Contracts\Query<\App\Contracts\QueryInputEmpty, \Illuminate\Support\Collection<int, \App\Dto\ProductSummaryDto>>
|
|
||||||
*/
|
|
||||||
final readonly class GetProductsSummariesQuery implements Query
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private ProductDirectory $productDirectory,
|
|
||||||
private PublicS3Disk $publicS3Disk
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return \Illuminate\Support\Collection<int, \App\Dto\ProductSummaryDto>
|
|
||||||
*/
|
|
||||||
public function __invoke(QueryInput $input): Collection
|
|
||||||
{
|
|
||||||
Assert::isAOf(
|
|
||||||
$input,
|
|
||||||
QueryInputEmpty::class,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Collection::make($this->productDirectory->getAllProductsSummaries())
|
|
||||||
->map(fn (ProductSummary $product): ProductSummaryDto => $this->mapToDto($product));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function mapToDto(ProductSummary $product): ProductSummaryDto
|
|
||||||
{
|
|
||||||
return new ProductSummaryDto(
|
|
||||||
id: $product->id,
|
|
||||||
name: $product->name,
|
|
||||||
pictureUrl: $product->picture ?
|
|
||||||
$this->publicS3Disk->publicUrlOf($product->picture) : null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Products;
|
|
||||||
|
|
||||||
use App\Contracts\Directories\ProductDirectory;
|
|
||||||
use App\Contracts\Query;
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
use App\Dto\ProductDto;
|
|
||||||
use App\Lib\PublicS3Disk;
|
|
||||||
use Domain\Models\Entities\Product as DomainProduct;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @implements \App\Contracts\Query<\App\Cqs\Queries\Products\GetRelatedProductsQueryInput, array<\App\Dto\ProductDto>>
|
|
||||||
*/
|
|
||||||
final readonly class GetRelatedProductsQuery implements Query
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private ProductDirectory $productDirectory,
|
|
||||||
private PublicS3Disk $publicS3Disk,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array< ProductDto>
|
|
||||||
*/
|
|
||||||
public function __invoke(QueryInput $input): array
|
|
||||||
{
|
|
||||||
Assert::isAOf($input, GetRelatedProductsQueryInput::class);
|
|
||||||
|
|
||||||
$products = $this->productDirectory->getRelatedProducts($input->recipeId, $input->limit);
|
|
||||||
|
|
||||||
return array_map(
|
|
||||||
fn (DomainProduct $product): ProductDto => ProductDto::fromEntity(
|
|
||||||
$product,
|
|
||||||
$this->publicS3Disk
|
|
||||||
),
|
|
||||||
$products
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Products;
|
|
||||||
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
final readonly class GetRelatedProductsQueryInput implements QueryInput
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public RecipeId $recipeId,
|
|
||||||
public int $limit = 10,
|
|
||||||
) {
|
|
||||||
Assert::greaterThan($this->limit, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Recipes;
|
|
||||||
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
use Ramsey\Uuid\UuidInterface;
|
|
||||||
|
|
||||||
final readonly class FindRecipeByIdInput implements QueryInput
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public UuidInterface $id,
|
|
||||||
public bool $includeTrashed = false
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Recipes;
|
|
||||||
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
|
|
||||||
final readonly class FindRecipeBySlugInput implements QueryInput
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public string $slug,
|
|
||||||
public bool $includeTrashed = false
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Recipes;
|
|
||||||
|
|
||||||
use App\Contracts\Query;
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
use App\Dto\RecipeDto;
|
|
||||||
use App\Lib\PublicS3Disk;
|
|
||||||
use App\Models\Recipe;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @phpcs:disable Generic.Files.LineLength
|
|
||||||
*
|
|
||||||
* @implements \App\Contracts\Query<\App\Cqs\Queries\Recipes\FindRecipeByIdInput|\App\Cqs\Queries\Recipes\FindRecipeBySlugInput, \App\Dto\RecipeDto|null>
|
|
||||||
*
|
|
||||||
* @phpcs:enable
|
|
||||||
*/
|
|
||||||
final readonly class FindRecipeQuery implements Query
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private PublicS3Disk $publicS3Disk,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
public function __invoke(QueryInput $input): ?RecipeDto
|
|
||||||
{
|
|
||||||
Assert::true(
|
|
||||||
$input instanceof FindRecipeByIdInput || $input instanceof FindRecipeBySlugInput,
|
|
||||||
'Input must be an instance of FindRecipeByIdInput or FindRecipeBySlugInput.'
|
|
||||||
);
|
|
||||||
|
|
||||||
$query = Recipe::query();
|
|
||||||
|
|
||||||
if ($input->includeTrashed) {
|
|
||||||
$query->withTrashed();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var Recipe|null $recipe */
|
|
||||||
$recipe = match (true) {
|
|
||||||
$input instanceof FindRecipeByIdInput => $query->where('id', (string) $input->id)->first(),
|
|
||||||
$input instanceof FindRecipeBySlugInput => $query->where('slug', $input->slug)->first(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if ($recipe === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return RecipeDto::fromModel($recipe, $this->publicS3Disk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Recipes;
|
|
||||||
|
|
||||||
use App\Contracts\Query;
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
use App\Models\Recipe;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
|
||||||
use Domain\Models\ValueObjects\Recipes\ConsumptionTime;
|
|
||||||
use Domain\Models\ValueObjects\Recipes\Inspiration;
|
|
||||||
use Domain\Models\ValueObjects\Recipes\Season;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use SortDirection;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* phpcs:disable Generic.Files.LineLength
|
|
||||||
*
|
|
||||||
* @implements \App\Contracts\Query<\App\Cqs\Queries\Recipes\GetBoPaginatedRecipesQueryInput, \App\Cqs\Queries\Recipes\GetBoPaginatedRecipesQueryOutput>
|
|
||||||
* phpcs:enable
|
|
||||||
*/
|
|
||||||
final readonly class GetBoPaginatedRecipesQuery implements Query
|
|
||||||
{
|
|
||||||
private const array SORTABLE_COLUMNS = [
|
|
||||||
'name',
|
|
||||||
'product_category',
|
|
||||||
'consomption_time',
|
|
||||||
'inspiration',
|
|
||||||
'season',
|
|
||||||
'is_published',
|
|
||||||
'created_at',
|
|
||||||
'updated_at',
|
|
||||||
];
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
public function __invoke(QueryInput $input): GetBoPaginatedRecipesQueryOutput
|
|
||||||
{
|
|
||||||
Assert::isAOf($input, GetBoPaginatedRecipesQueryInput::class);
|
|
||||||
|
|
||||||
/** @var Builder<Recipe> $query */
|
|
||||||
$query = Recipe::query();
|
|
||||||
|
|
||||||
if ($input->isTrash) {
|
|
||||||
$query->onlyTrashed();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($input->search !== null && $input->search !== '') {
|
|
||||||
$words = array_filter(explode(' ', $input->search));
|
|
||||||
$query->where(function (Builder $q) use ($words, $input): void {
|
|
||||||
foreach ($words as $word) {
|
|
||||||
$q->where('name', 'like', "%{$word}%");
|
|
||||||
}
|
|
||||||
$q->orWhere('name', 'like', "%{$input->search}%");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$sortCol = in_array($input->sortCol, self::SORTABLE_COLUMNS, true)
|
|
||||||
? $input->sortCol
|
|
||||||
: 'name';
|
|
||||||
|
|
||||||
$direction = $input->sortDirection === SortDirection::Descending ? 'desc' : 'asc';
|
|
||||||
$query->orderBy($sortCol, $direction);
|
|
||||||
|
|
||||||
$paginated = $query->paginate(
|
|
||||||
perPage: $input->pagination->pageSize(),
|
|
||||||
page: $input->pagination->pageNumber(),
|
|
||||||
)->withQueryString();
|
|
||||||
|
|
||||||
$searchFields = \implode(', ', [
|
|
||||||
trans('validation.attributes.name'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return new GetBoPaginatedRecipesQueryOutput(
|
|
||||||
paginatedRecipes: $paginated,
|
|
||||||
search: $input->search,
|
|
||||||
searchFields: $searchFields,
|
|
||||||
categories: ProductCategory::toArray(),
|
|
||||||
consumptionTimes: ConsumptionTime::toArray(),
|
|
||||||
inspirations: Inspiration::toArray(),
|
|
||||||
seasons: Season::toArray(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Recipes;
|
|
||||||
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
use Domain\Models\ValueObjects\Pagination;
|
|
||||||
use SortDirection;
|
|
||||||
|
|
||||||
final readonly class GetBoPaginatedRecipesQueryInput implements QueryInput
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public Pagination $pagination,
|
|
||||||
public ?string $search = null,
|
|
||||||
public string $sortCol = 'name',
|
|
||||||
public SortDirection $sortDirection = SortDirection::Ascending,
|
|
||||||
public bool $isTrash = false,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Recipes;
|
|
||||||
|
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
||||||
|
|
||||||
final readonly class GetBoPaginatedRecipesQueryOutput
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param LengthAwarePaginator<int,\App\Models\Recipe> $paginatedRecipes
|
|
||||||
* @param array<string, mixed> $categories
|
|
||||||
* @param array<string, mixed> $consumptionTimes
|
|
||||||
* @param array<string, mixed> $inspirations
|
|
||||||
* @param array<string, mixed> $seasons
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
public LengthAwarePaginator $paginatedRecipes,
|
|
||||||
public ?string $search,
|
|
||||||
public string $searchFields,
|
|
||||||
public array $categories,
|
|
||||||
public array $consumptionTimes,
|
|
||||||
public array $inspirations,
|
|
||||||
public array $seasons,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Recipes;
|
|
||||||
|
|
||||||
use App\Contracts\Directories\RecipeDirectory;
|
|
||||||
use App\Contracts\Query;
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
use App\Dto\RecipeDto;
|
|
||||||
use App\Lib\PublicS3Disk;
|
|
||||||
use Domain\Models\ValueObjects\Pagination;
|
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* phpcs:disable Generic.Files.LineLength.TooLong
|
|
||||||
*
|
|
||||||
* @implements \App\Contracts\Query<\App\Cqs\Queries\Recipes\GetFrontRecipesQueryInput, \Illuminate\Pagination\LengthAwarePaginator<array-key, \App\Dto\RecipeDto>>
|
|
||||||
*
|
|
||||||
* phpcs:enable
|
|
||||||
*/
|
|
||||||
final readonly class GetFrontRecipesQuery implements Query
|
|
||||||
{
|
|
||||||
private const int PER_PAGE = 100;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
private RecipeDirectory $recipeDirectory,
|
|
||||||
private PublicS3Disk $publicS3Disk,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return LengthAwarePaginator<array-key, \App\Dto\RecipeDto>
|
|
||||||
*/
|
|
||||||
public function __invoke(QueryInput $input): LengthAwarePaginator
|
|
||||||
{
|
|
||||||
Assert::isAOf($input, GetFrontRecipesQueryInput::class);
|
|
||||||
|
|
||||||
// * Load all recipes for client-side display
|
|
||||||
$allRecipes = [];
|
|
||||||
$page = 1;
|
|
||||||
|
|
||||||
do {
|
|
||||||
$pagination = Pagination::ofPage($page, self::PER_PAGE);
|
|
||||||
|
|
||||||
$paginatedRecipes = $this->recipeDirectory->getPaginatedRecipes(
|
|
||||||
pagination: $pagination,
|
|
||||||
productRanges: $input->categories,
|
|
||||||
consumptionTimes: $input->consumptionTimes,
|
|
||||||
inspirations: $input->inspirations,
|
|
||||||
seasons: $input->seasons,
|
|
||||||
search: $input->search
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($paginatedRecipes->items() as $recipe) {
|
|
||||||
$allRecipes[] = RecipeDto::fromEntity($recipe, $this->publicS3Disk);
|
|
||||||
}
|
|
||||||
|
|
||||||
$page++;
|
|
||||||
} while ($page <= $paginatedRecipes->lastPage());
|
|
||||||
|
|
||||||
return new LengthAwarePaginator(
|
|
||||||
items: $allRecipes,
|
|
||||||
total: count($allRecipes),
|
|
||||||
perPage: max(1, count($allRecipes)),
|
|
||||||
currentPage: 1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Recipes;
|
|
||||||
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
|
||||||
use Domain\Models\ValueObjects\Recipes\ConsumptionTime;
|
|
||||||
use Domain\Models\ValueObjects\Recipes\Inspiration;
|
|
||||||
use Domain\Models\ValueObjects\Recipes\Season;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
final readonly class GetFrontRecipesQueryInput implements QueryInput
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param ProductCategory[] $categories
|
|
||||||
* @param ConsumptionTime[] $consumptionTimes
|
|
||||||
* @param Inspiration[] $inspirations
|
|
||||||
* @param Season[] $seasons
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
public array $categories = [],
|
|
||||||
public array $consumptionTimes = [],
|
|
||||||
public array $inspirations = [],
|
|
||||||
public array $seasons = [],
|
|
||||||
public string $search = '',
|
|
||||||
) {
|
|
||||||
Assert::allIsAOf($this->categories, ProductCategory::class);
|
|
||||||
Assert::allIsAOf($this->consumptionTimes, ConsumptionTime::class);
|
|
||||||
Assert::allIsAOf($this->inspirations, Inspiration::class);
|
|
||||||
Assert::allIsAOf($this->seasons, Season::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Recipes;
|
|
||||||
|
|
||||||
use App\Contracts\Directories\RecipeDirectory;
|
|
||||||
use App\Contracts\Query;
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
use App\Dto\RecipeDto;
|
|
||||||
use App\Lib\PublicS3Disk;
|
|
||||||
use Domain\Models\Entities\Recipe as DomainRecipe;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @implements \App\Contracts\Query<\App\Cqs\Queries\Recipes\GetRelatedRecipesQueryInput, array<\App\Dto\RecipeDto>>
|
|
||||||
*/
|
|
||||||
final readonly class GetRelatedRecipesQuery implements Query
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private RecipeDirectory $recipeDirectory,
|
|
||||||
private PublicS3Disk $publicS3Disk,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<RecipeDto>
|
|
||||||
*/
|
|
||||||
public function __invoke(QueryInput $input): array
|
|
||||||
{
|
|
||||||
Assert::isAOf($input, GetRelatedRecipesQueryInput::class);
|
|
||||||
|
|
||||||
$recipes = $this->recipeDirectory->getRelatedRecipes($input->productId, $input->limit);
|
|
||||||
|
|
||||||
return array_map(
|
|
||||||
fn (DomainRecipe $recipe): RecipeDto => RecipeDto::fromEntity(
|
|
||||||
$recipe,
|
|
||||||
$this->publicS3Disk
|
|
||||||
),
|
|
||||||
$recipes
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Cqs\Queries\Recipes;
|
|
||||||
|
|
||||||
use App\Contracts\QueryInput;
|
|
||||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
final readonly class GetRelatedRecipesQueryInput implements QueryInput
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public ProductId $productId,
|
|
||||||
public int $limit = 10,
|
|
||||||
) {
|
|
||||||
Assert::greaterThan($this->limit, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,228 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Directories;
|
|
||||||
|
|
||||||
use App\Contracts\Directories\ProductDirectory;
|
|
||||||
use App\Dto\CategoryProductsDto;
|
|
||||||
use App\Models\Product;
|
|
||||||
use App\Models\Recipe;
|
|
||||||
use Domain\Models\Entities\Product as DomainProduct;
|
|
||||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
|
||||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
|
||||||
use Domain\Models\ValueObjects\Pagination;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductSummary;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
|
||||||
use Ramsey\Uuid\Uuid;
|
|
||||||
use Ramsey\Uuid\UuidInterface;
|
|
||||||
|
|
||||||
class EloquentProductDirectory implements ProductDirectory
|
|
||||||
{
|
|
||||||
public function find(ProductId $id): ?DomainProduct
|
|
||||||
{
|
|
||||||
return Product::query()
|
|
||||||
->with(['recipes'])
|
|
||||||
->where('id', (string) $id->value)
|
|
||||||
->limit(1)
|
|
||||||
->get()
|
|
||||||
->map(fn (Product $product) => $this->mapToEntity($product))
|
|
||||||
->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function findBySlug(string $slug): ?DomainProduct
|
|
||||||
{
|
|
||||||
return Product::query()
|
|
||||||
->with(['recipes'])
|
|
||||||
->where('slug', $slug)
|
|
||||||
->limit(1)
|
|
||||||
->get()
|
|
||||||
->map(fn (Product $product) => $this->mapToEntity($product))
|
|
||||||
->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAllProductsSummaries(): array
|
|
||||||
{
|
|
||||||
return Product::query()
|
|
||||||
->select(['id', 'name', 'picture'])
|
|
||||||
->orderBy('name', 'asc')
|
|
||||||
->get()
|
|
||||||
->map(fn (Product $product) => $this->mapToSummary($product))
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPaginatedProducts(
|
|
||||||
Pagination $pagination,
|
|
||||||
string $search = '',
|
|
||||||
): LengthAwarePaginator {
|
|
||||||
$dbQueryCallback = function (Builder $query) {
|
|
||||||
/** @var \Illuminate\Database\Eloquent\Builder<Product> $query */
|
|
||||||
$query->with(['recipes']);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (! empty($search)) {
|
|
||||||
$scout = Product::search($search)->query($dbQueryCallback);
|
|
||||||
|
|
||||||
/** @var \Illuminate\Pagination\LengthAwarePaginator<array-key,Product> $paginated */
|
|
||||||
$paginated = $scout->paginate(
|
|
||||||
$pagination->pageSize(),
|
|
||||||
'page',
|
|
||||||
$pagination->pageNumber()
|
|
||||||
);
|
|
||||||
|
|
||||||
return $paginated->through(fn ($product) => $this->mapToEntity($product));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var \Illuminate\Database\Eloquent\Builder<Product> $query */
|
|
||||||
$query = Product::query();
|
|
||||||
$dbQueryCallback($query);
|
|
||||||
|
|
||||||
return $query->paginate(
|
|
||||||
perPage: $pagination->pageSize(),
|
|
||||||
page: $pagination->pageNumber()
|
|
||||||
)->through(fn (Product $product) => $this->mapToEntity($product));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \Domain\Models\ValueObjects\Products\BreedingMethod[] $breedingMethods
|
|
||||||
* @param \Domain\Models\ValueObjects\Products\Technology[] $technologies
|
|
||||||
* @param \Domain\Models\ValueObjects\Products\Business[] $businesses
|
|
||||||
*/
|
|
||||||
public function getCategoryProducts(
|
|
||||||
ProductCategory $category,
|
|
||||||
array $breedingMethods = [],
|
|
||||||
array $technologies = [],
|
|
||||||
array $businesses = [],
|
|
||||||
?bool $isEgalim = null,
|
|
||||||
string $search = '',
|
|
||||||
int $limitPerCategory = 6
|
|
||||||
): ?CategoryProductsDto {
|
|
||||||
|
|
||||||
$dbQueryCallback = function (Builder $query) use ($category, $breedingMethods, $technologies, $businesses, $isEgalim) {
|
|
||||||
/** @var \Illuminate\Database\Eloquent\Builder<Product> $query */
|
|
||||||
$query->where('category', $category->value)
|
|
||||||
->where('is_published', true)
|
|
||||||
->with(['recipes']);
|
|
||||||
|
|
||||||
if (! empty($breedingMethods)) {
|
|
||||||
$query->where(function ($q) use ($breedingMethods) {
|
|
||||||
foreach ($breedingMethods as $method) {
|
|
||||||
$q->orWhereJsonContains('breeding_methods', $method->value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($technologies)) {
|
|
||||||
$techValues = array_map(fn ($t) => $t->value, $technologies);
|
|
||||||
$query->whereIn('technology', $techValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($businesses)) {
|
|
||||||
$query->where(function ($q) use ($businesses) {
|
|
||||||
foreach ($businesses as $business) {
|
|
||||||
$q->orWhereJsonContains('businesses', $business->value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($isEgalim === true) {
|
|
||||||
$query->where('is_egalim', true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$query = Product::query();
|
|
||||||
|
|
||||||
if (! empty($search)) {
|
|
||||||
$scout = Product::search($search);
|
|
||||||
// Get all matching IDs from Meilisearch
|
|
||||||
$matchingIds = $scout->keys();
|
|
||||||
|
|
||||||
if ($matchingIds->isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$query->whereIn('id', $matchingIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
$dbQueryCallback($query);
|
|
||||||
$count = $query->count();
|
|
||||||
|
|
||||||
if ($count === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$products = (clone $query)->limit($limitPerCategory)
|
|
||||||
->get()
|
|
||||||
->map(fn (Product $product) => $this->mapToEntity($product))
|
|
||||||
->all();
|
|
||||||
|
|
||||||
return new CategoryProductsDto(
|
|
||||||
category: $category,
|
|
||||||
total: $count,
|
|
||||||
products: $products,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function mapToSummary(Product $product): ProductSummary
|
|
||||||
{
|
|
||||||
return new ProductSummary(
|
|
||||||
id: (string) $product->id,
|
|
||||||
name: $product->name,
|
|
||||||
picture: $product->picture?->value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function mapToEntity(Product $product): DomainProduct
|
|
||||||
{
|
|
||||||
return new DomainProduct(
|
|
||||||
id: new ProductId(Uuid::fromString($product->id)),
|
|
||||||
name: $product->name,
|
|
||||||
slug: $product->slug,
|
|
||||||
category: $product->category,
|
|
||||||
technology: $product->technology,
|
|
||||||
isEgalim: $product->is_egalim,
|
|
||||||
isPublished: (bool) $product->is_published,
|
|
||||||
breedingMethods: $product->breeding_methods->all(),
|
|
||||||
businesses: $product->businesses->all(),
|
|
||||||
detailsAndAdvantages: $product->details_and_advantages,
|
|
||||||
formats: $product->formats->all(),
|
|
||||||
handlingAdvices: $product->handling_advices,
|
|
||||||
ingredients: $product->ingredients,
|
|
||||||
conservationBeforeOpening: $product->conservation_before_opening,
|
|
||||||
description: $product->description,
|
|
||||||
seoTitle: $product->seo_title,
|
|
||||||
seoDescription: $product->seo_description,
|
|
||||||
conservationAfterOpening: $product->conservation_after_opening,
|
|
||||||
nutritionalValues: $product->nutritional_values,
|
|
||||||
picture: $product->picture?->value,
|
|
||||||
pictureAlt: $product->picture_alt,
|
|
||||||
pictureTitle: $product->picture_title,
|
|
||||||
pdfFilePath: $product->pdf?->value,
|
|
||||||
associatedRecipes: $product->relationLoaded('recipes')
|
|
||||||
? $product->recipes->map(
|
|
||||||
fn (Recipe $recipe): UuidInterface => $recipe->getId()->value
|
|
||||||
)->all()
|
|
||||||
: [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRelatedProducts(RecipeId $recipeId, int $limit = 4): array
|
|
||||||
{
|
|
||||||
$recipe = Recipe::query()
|
|
||||||
->where('id', $recipeId->value)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (! $recipe) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $recipe->products()
|
|
||||||
->where('is_published', true)
|
|
||||||
->limit($limit)
|
|
||||||
->get()
|
|
||||||
->map(fn (Product $product) => $this->mapToEntity($product))
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Directories;
|
|
||||||
|
|
||||||
use App\Contracts\Directories\RecipeDirectory;
|
|
||||||
use App\Models\Product;
|
|
||||||
use App\Models\Recipe;
|
|
||||||
use Domain\Models\Entities\Recipe as DomainRecipe;
|
|
||||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
|
||||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
|
||||||
use Domain\Models\ValueObjects\Pagination;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
|
||||||
use Ramsey\Uuid\Uuid;
|
|
||||||
|
|
||||||
class EloquentRecipeDirectory implements RecipeDirectory
|
|
||||||
{
|
|
||||||
public function find(RecipeId $id): ?DomainRecipe
|
|
||||||
{
|
|
||||||
return Recipe::query()
|
|
||||||
->with(['products'])
|
|
||||||
->where('id', (string) $id->value)
|
|
||||||
->limit(1)
|
|
||||||
->get()
|
|
||||||
->map(fn (Recipe $recipe) => $this->mapToEntity($recipe))
|
|
||||||
->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function findBySlug(string $slug): ?DomainRecipe
|
|
||||||
{
|
|
||||||
return Recipe::query()
|
|
||||||
->with(['products'])
|
|
||||||
->where('slug', $slug)
|
|
||||||
->limit(1)
|
|
||||||
->get()
|
|
||||||
->map(fn (Recipe $recipe) => $this->mapToEntity($recipe))
|
|
||||||
->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPaginatedRecipes(
|
|
||||||
Pagination $pagination,
|
|
||||||
array $productRanges = [],
|
|
||||||
array $consumptionTimes = [],
|
|
||||||
array $inspirations = [],
|
|
||||||
array $seasons = [],
|
|
||||||
string $search = ''
|
|
||||||
): LengthAwarePaginator {
|
|
||||||
$dbQueryCallback = function (Builder $query) use ($productRanges, $consumptionTimes, $inspirations, $seasons) {
|
|
||||||
/** @var \Illuminate\Database\Eloquent\Builder<Recipe> $query */
|
|
||||||
$query->with(['products'])
|
|
||||||
->where('is_published', true);
|
|
||||||
|
|
||||||
if (! empty($productRanges)) {
|
|
||||||
$rangesValues = array_map(fn ($r) => $r->value, $productRanges);
|
|
||||||
$query->whereIn('product_category', $rangesValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($consumptionTimes)) {
|
|
||||||
$timesValues = array_map(fn ($t) => $t->value, $consumptionTimes);
|
|
||||||
$query->whereIn('consomption_time', $timesValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($inspirations)) {
|
|
||||||
$inspValues = array_map(fn ($i) => $i->value, $inspirations);
|
|
||||||
$query->whereIn('inspiration', $inspValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($seasons)) {
|
|
||||||
$seasonValues = array_map(fn ($s) => $s->value, $seasons);
|
|
||||||
$query->whereIn('season', $seasonValues);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$query = Recipe::query();
|
|
||||||
|
|
||||||
if (! empty($search)) {
|
|
||||||
$scout = Recipe::search($search);
|
|
||||||
$matchingIds = $scout->keys();
|
|
||||||
|
|
||||||
if ($matchingIds->isEmpty()) {
|
|
||||||
/** @var \Illuminate\Pagination\LengthAwarePaginator<array-key,DomainRecipe> */
|
|
||||||
return $query->whereIn('id', [])->paginate(
|
|
||||||
perPage: $pagination->pageSize(),
|
|
||||||
page: $pagination->pageNumber()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$query->whereIn('id', $matchingIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
$dbQueryCallback($query);
|
|
||||||
|
|
||||||
return $query->paginate(
|
|
||||||
perPage: $pagination->pageSize(),
|
|
||||||
page: $pagination->pageNumber()
|
|
||||||
)->through(fn (Recipe $recipe) => $this->mapToEntity($recipe));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function mapToEntity(Recipe $recipe): DomainRecipe
|
|
||||||
{
|
|
||||||
return new DomainRecipe(
|
|
||||||
id: new RecipeId(Uuid::fromString($recipe->id)),
|
|
||||||
name: $recipe->name,
|
|
||||||
slug: $recipe->slug,
|
|
||||||
isPublished: (bool) $recipe->is_published,
|
|
||||||
picture: $recipe->picture?->value,
|
|
||||||
seoTitle: $recipe->seo_title,
|
|
||||||
seoDescription: $recipe->seo_description,
|
|
||||||
productRange: $recipe->product_category,
|
|
||||||
consomptionTime: $recipe->consomption_time,
|
|
||||||
inspiration: $recipe->inspiration,
|
|
||||||
season: $recipe->season,
|
|
||||||
description: $recipe->description,
|
|
||||||
details: $recipe->details,
|
|
||||||
preparationTime: '15 min de préparation',
|
|
||||||
guestsCount: 4,
|
|
||||||
ingredientsList: $recipe->ingredients_list,
|
|
||||||
preparationSteps: $recipe->preparation_steps,
|
|
||||||
tipsAndAdvices: $recipe->tips_and_advices,
|
|
||||||
associatedProducts: []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRelatedRecipes(ProductId $productId, int $limit = 4): array
|
|
||||||
{
|
|
||||||
$product = Product::query()
|
|
||||||
->where('id', (string) $productId->value)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (! $product) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $product->recipes()
|
|
||||||
->where('is_published', true)
|
|
||||||
->limit($limit)
|
|
||||||
->get()
|
|
||||||
->map(fn (Recipe $recipe) => $this->mapToEntity($recipe))
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Dto;
|
|
||||||
|
|
||||||
use App\Lib\PublicS3Disk;
|
|
||||||
use Domain\Models\Entities\Product;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
use Override;
|
|
||||||
|
|
||||||
final readonly class CategoryProductsDto implements \JsonSerializable
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public ProductCategory $category,
|
|
||||||
public int $total,
|
|
||||||
/** @var \Domain\Models\Entities\Product[] */
|
|
||||||
public array $products,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Override]
|
|
||||||
public function jsonSerialize(): mixed
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'category' => [
|
|
||||||
'name' => $this->category->name(),
|
|
||||||
'label' => $this->category->label(),
|
|
||||||
'value' => $this->category->value(),
|
|
||||||
],
|
|
||||||
'total' => $this->total,
|
|
||||||
'products' => array_map(
|
|
||||||
[$this, 'productsToArray'],
|
|
||||||
$this->products
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<array-key,mixed> */
|
|
||||||
private function productsToArray(Product $product): array
|
|
||||||
{
|
|
||||||
$publicS3Disk = app(PublicS3Disk::class);
|
|
||||||
$data = Arr::wrap(json_decode(json_encode($product) ?: '', true));
|
|
||||||
if (isset($data['picture']) && is_string($data['picture'])) {
|
|
||||||
$data['picture'] = $publicS3Disk->publicUrlOf($data['picture']);
|
|
||||||
}
|
|
||||||
if (isset($data['pdfFilePath']) && is_string($data['pdfFilePath'])) {
|
|
||||||
$data['pdfFilePath'] = $publicS3Disk->publicUrlOf($data['pdfFilePath']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_merge(
|
|
||||||
$data,
|
|
||||||
[
|
|
||||||
'id' => (string) $product->id->value,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,242 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Dto;
|
|
||||||
|
|
||||||
use App\Lib\PublicS3Disk;
|
|
||||||
use App\Models\Product;
|
|
||||||
use Carbon\CarbonInterface;
|
|
||||||
use Domain\Models\Entities\Product as DomainProduct;
|
|
||||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
|
||||||
use Domain\Models\ValueObjects\Products\NutritionalValues;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
|
||||||
use Domain\Models\ValueObjects\Products\Technology;
|
|
||||||
use Illuminate\Contracts\Routing\UrlRoutable;
|
|
||||||
use Illuminate\Contracts\Support\Arrayable;
|
|
||||||
use Ramsey\Uuid\Uuid;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @implements Arrayable<string, mixed>
|
|
||||||
*/
|
|
||||||
final readonly class ProductDto implements \JsonSerializable, Arrayable, UrlRoutable
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param \Domain\Models\ValueObjects\Products\BreedingMethod[] $breedingMethods
|
|
||||||
* @param \Domain\Models\ValueObjects\Products\Business[] $businesses
|
|
||||||
* @param \Domain\Models\ValueObjects\Products\ProductFormat[] $formats
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
public string $id,
|
|
||||||
public string $seoTitle,
|
|
||||||
public string $seoDescription,
|
|
||||||
public string $name,
|
|
||||||
public ?string $picture,
|
|
||||||
public ?string $pictureUrl,
|
|
||||||
public string $pictureAlt,
|
|
||||||
public string $pictureTitle,
|
|
||||||
public string $slug,
|
|
||||||
public ProductCategory $category,
|
|
||||||
public Technology $technology,
|
|
||||||
public bool $isEgalim,
|
|
||||||
public bool $isPublished,
|
|
||||||
public array $breedingMethods,
|
|
||||||
public array $businesses,
|
|
||||||
public array $formats,
|
|
||||||
public string $description,
|
|
||||||
public string $detailsAndAdvantages,
|
|
||||||
public string $handlingAdvices,
|
|
||||||
public ?string $ingredients,
|
|
||||||
public string $conservationBeforeOpening,
|
|
||||||
public ?string $conservationAfterOpening,
|
|
||||||
public ?NutritionalValues $nutritionalValues,
|
|
||||||
public ?string $pdf,
|
|
||||||
public ?string $pdfUrl,
|
|
||||||
public ?CarbonInterface $createdAt,
|
|
||||||
public ?CarbonInterface $updatedAt,
|
|
||||||
public ?CarbonInterface $deletedAt,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromModel(Product $product, PublicS3Disk $publicS3Disk): self
|
|
||||||
{
|
|
||||||
$picturePath = $product->picture?->value;
|
|
||||||
$pdfPath = $product->pdf?->value;
|
|
||||||
|
|
||||||
return new self(
|
|
||||||
id: (string) $product->id,
|
|
||||||
seoTitle: (string) $product->seo_title,
|
|
||||||
seoDescription: (string) $product->seo_description,
|
|
||||||
name: (string) $product->name,
|
|
||||||
picture: $picturePath,
|
|
||||||
pictureUrl: $picturePath !== null ? $publicS3Disk->publicUrlOf($picturePath) : null,
|
|
||||||
pictureAlt: (string) $product->picture_alt,
|
|
||||||
pictureTitle: (string) $product->picture_title,
|
|
||||||
slug: (string) $product->slug,
|
|
||||||
category: $product->category,
|
|
||||||
technology: $product->technology,
|
|
||||||
isEgalim: (bool) $product->is_egalim,
|
|
||||||
isPublished: (bool) $product->is_published,
|
|
||||||
breedingMethods: $product->breeding_methods->values()->all(),
|
|
||||||
businesses: $product->businesses->values()->all(),
|
|
||||||
formats: $product->formats->values()->all(),
|
|
||||||
description: (string) $product->description,
|
|
||||||
detailsAndAdvantages: (string) $product->details_and_advantages,
|
|
||||||
handlingAdvices: (string) $product->handling_advices,
|
|
||||||
ingredients: $product->ingredients,
|
|
||||||
conservationBeforeOpening: (string) $product->conservation_before_opening,
|
|
||||||
conservationAfterOpening: $product->conservation_after_opening,
|
|
||||||
nutritionalValues: $product->nutritional_values,
|
|
||||||
pdf: $pdfPath,
|
|
||||||
pdfUrl: $pdfPath !== null ? $publicS3Disk->publicUrlOf($pdfPath) : null,
|
|
||||||
createdAt: $product->created_at,
|
|
||||||
updatedAt: $product->updated_at,
|
|
||||||
deletedAt: $product->deleted_at,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromEntity(
|
|
||||||
DomainProduct $product,
|
|
||||||
PublicS3Disk $publicS3Disk
|
|
||||||
): self {
|
|
||||||
$picturePath = $product->picture;
|
|
||||||
$pdfPath = $product->pdfFilePath;
|
|
||||||
|
|
||||||
return new self(
|
|
||||||
id: (string) $product->id->value,
|
|
||||||
seoTitle: (string) ($product->seoTitle ?? ''),
|
|
||||||
seoDescription: (string) ($product->seoDescription ?? ''),
|
|
||||||
name: $product->name,
|
|
||||||
picture: $picturePath,
|
|
||||||
pictureUrl: $picturePath !== null ? $publicS3Disk->publicUrlOf($picturePath) : null,
|
|
||||||
pictureAlt: (string) ($product->pictureAlt ?? ''),
|
|
||||||
pictureTitle: (string) ($product->pictureTitle ?? ''),
|
|
||||||
slug: $product->slug,
|
|
||||||
category: $product->category,
|
|
||||||
technology: $product->technology,
|
|
||||||
isEgalim: $product->isEgalim,
|
|
||||||
isPublished: $product->isPublished,
|
|
||||||
breedingMethods: $product->breedingMethods,
|
|
||||||
businesses: $product->businesses,
|
|
||||||
formats: $product->formats,
|
|
||||||
description: $product->description,
|
|
||||||
detailsAndAdvantages: $product->detailsAndAdvantages,
|
|
||||||
handlingAdvices: $product->handlingAdvices,
|
|
||||||
ingredients: $product->ingredients,
|
|
||||||
conservationBeforeOpening: $product->conservationBeforeOpening,
|
|
||||||
conservationAfterOpening: $product->conservationAfterOpening,
|
|
||||||
nutritionalValues: $product->nutritionalValues,
|
|
||||||
pdf: $pdfPath,
|
|
||||||
pdfUrl: $pdfPath !== null ? $publicS3Disk->publicUrlOf($pdfPath) : null,
|
|
||||||
createdAt: null,
|
|
||||||
updatedAt: null,
|
|
||||||
deletedAt: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function dummyForZiggy(): self
|
|
||||||
{
|
|
||||||
return new self(
|
|
||||||
id: 'dummy-id',
|
|
||||||
seoTitle: 'dummy-seo-title',
|
|
||||||
seoDescription: 'dummy-seo-description',
|
|
||||||
name: 'dummy-name',
|
|
||||||
picture: null,
|
|
||||||
pictureUrl: null,
|
|
||||||
pictureAlt: 'dummy-picture-alt',
|
|
||||||
pictureTitle: 'dummy-picture-title',
|
|
||||||
slug: 'dummy-slug',
|
|
||||||
category: ProductCategory::OMELETS_AND_DELI_AND_DESSERT_LINES,
|
|
||||||
technology: Technology::FRESH,
|
|
||||||
isEgalim: false,
|
|
||||||
isPublished: true,
|
|
||||||
breedingMethods: [],
|
|
||||||
businesses: [],
|
|
||||||
formats: [],
|
|
||||||
description: 'dummy-description',
|
|
||||||
detailsAndAdvantages: 'dummy-details-and-advantages',
|
|
||||||
handlingAdvices: 'dummy-handling-advices',
|
|
||||||
ingredients: 'dummy-ingredients',
|
|
||||||
conservationBeforeOpening: 'dummy-conservation-before-opening',
|
|
||||||
conservationAfterOpening: 'dummy-conservation-after-opening',
|
|
||||||
nutritionalValues: null,
|
|
||||||
pdf: null,
|
|
||||||
pdfUrl: null,
|
|
||||||
createdAt: null,
|
|
||||||
updatedAt: null,
|
|
||||||
deletedAt: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getId(): ProductId
|
|
||||||
{
|
|
||||||
return new ProductId(Uuid::fromString($this->id));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
public function getRouteKey(): mixed
|
|
||||||
{
|
|
||||||
return $this->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
public function getRouteKeyName(): string
|
|
||||||
{
|
|
||||||
return 'id';
|
|
||||||
}
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
public function resolveRouteBinding($value, $field = null): mixed
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
public function resolveChildRouteBinding($childType, $value, $field): mixed
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function toArray(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'id' => $this->id,
|
|
||||||
'name' => $this->name,
|
|
||||||
'slug' => $this->slug,
|
|
||||||
'seo_title' => $this->seoTitle,
|
|
||||||
'seo_description' => $this->seoDescription,
|
|
||||||
'picture' => $this->pictureUrl,
|
|
||||||
'picture_alt' => $this->pictureAlt,
|
|
||||||
'picture_title' => $this->pictureTitle,
|
|
||||||
'category' => $this->category->value,
|
|
||||||
'technology' => $this->technology->value,
|
|
||||||
'is_egalim' => $this->isEgalim,
|
|
||||||
'is_published' => $this->isPublished,
|
|
||||||
'breeding_methods' => $this->breedingMethods,
|
|
||||||
'businesses' => $this->businesses,
|
|
||||||
'formats' => $this->formats,
|
|
||||||
'description' => $this->description,
|
|
||||||
'details_and_advantages' => $this->detailsAndAdvantages,
|
|
||||||
'handling_advices' => $this->handlingAdvices,
|
|
||||||
'ingredients' => $this->ingredients,
|
|
||||||
'conservation_before_opening' => $this->conservationBeforeOpening,
|
|
||||||
'conservation_after_opening' => $this->conservationAfterOpening,
|
|
||||||
'nutritional_values' => $this->nutritionalValues?->toArray(),
|
|
||||||
'pdf' => $this->pdfUrl,
|
|
||||||
'created_at' => $this->createdAt?->toISOString(),
|
|
||||||
'updated_at' => $this->updatedAt?->toISOString(),
|
|
||||||
'deleted_at' => $this->deletedAt?->toISOString(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function jsonSerialize(): array
|
|
||||||
{
|
|
||||||
return $this->toArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Dto;
|
|
||||||
|
|
||||||
final readonly class ProductSummaryDto
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public string $id,
|
|
||||||
public string $name,
|
|
||||||
public ?string $pictureUrl,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Dto;
|
|
||||||
|
|
||||||
use App\Lib\PublicS3Disk;
|
|
||||||
use App\Models\Recipe;
|
|
||||||
use Carbon\CarbonInterface;
|
|
||||||
use Domain\Models\Entities\Recipe as DomainRecipe;
|
|
||||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
|
||||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
|
||||||
use Domain\Models\ValueObjects\Recipes\ConsumptionTime;
|
|
||||||
use Domain\Models\ValueObjects\Recipes\Inspiration;
|
|
||||||
use Domain\Models\ValueObjects\Recipes\Season;
|
|
||||||
use Illuminate\Contracts\Routing\UrlRoutable;
|
|
||||||
use Illuminate\Contracts\Support\Arrayable;
|
|
||||||
use JsonSerializable;
|
|
||||||
use Ramsey\Uuid\Uuid;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @implements Arrayable<string, mixed>
|
|
||||||
*/
|
|
||||||
final readonly class RecipeDto implements Arrayable, JsonSerializable, UrlRoutable
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public string $id,
|
|
||||||
public string $seoTitle,
|
|
||||||
public string $seoDescription,
|
|
||||||
public string $name,
|
|
||||||
public ?string $picture,
|
|
||||||
public ?string $pictureUrl,
|
|
||||||
public string $pictureAlt,
|
|
||||||
public string $pictureTitle,
|
|
||||||
public string $slug,
|
|
||||||
public bool $isPublished,
|
|
||||||
public ProductCategory $productCategory,
|
|
||||||
public ConsumptionTime $consomptionTime,
|
|
||||||
public Inspiration $inspiration,
|
|
||||||
public Season $season,
|
|
||||||
public ?string $description,
|
|
||||||
public ?string $details,
|
|
||||||
public string $ingredientsList,
|
|
||||||
public string $preparationSteps,
|
|
||||||
public ?string $tipsAndAdvices,
|
|
||||||
public ?CarbonInterface $createdAt,
|
|
||||||
public ?CarbonInterface $updatedAt,
|
|
||||||
public ?CarbonInterface $deletedAt,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromModel(Recipe $recipe, PublicS3Disk $publicS3Disk): self
|
|
||||||
{
|
|
||||||
$picturePath = $recipe->picture?->value;
|
|
||||||
|
|
||||||
return new self(
|
|
||||||
id: (string) $recipe->id,
|
|
||||||
seoTitle: (string) $recipe->seo_title,
|
|
||||||
seoDescription: (string) $recipe->seo_description,
|
|
||||||
name: (string) $recipe->name,
|
|
||||||
picture: $picturePath,
|
|
||||||
pictureUrl: $picturePath !== null ? $publicS3Disk->publicUrlOf($picturePath) : null,
|
|
||||||
pictureAlt: (string) $recipe->picture_alt,
|
|
||||||
pictureTitle: (string) $recipe->picture_title,
|
|
||||||
slug: (string) $recipe->slug,
|
|
||||||
isPublished: (bool) $recipe->is_published,
|
|
||||||
productCategory: $recipe->product_category,
|
|
||||||
consomptionTime: $recipe->consomption_time,
|
|
||||||
inspiration: $recipe->inspiration,
|
|
||||||
season: $recipe->season,
|
|
||||||
description: $recipe->description,
|
|
||||||
details: $recipe->details,
|
|
||||||
ingredientsList: (string) $recipe->ingredients_list,
|
|
||||||
preparationSteps: (string) $recipe->preparation_steps,
|
|
||||||
tipsAndAdvices: $recipe->tips_and_advices,
|
|
||||||
createdAt: $recipe->created_at,
|
|
||||||
updatedAt: $recipe->updated_at,
|
|
||||||
deletedAt: $recipe->deleted_at,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromEntity(
|
|
||||||
DomainRecipe $recipe,
|
|
||||||
PublicS3Disk $publicS3Disk
|
|
||||||
): self {
|
|
||||||
$picturePath = $recipe->picture;
|
|
||||||
|
|
||||||
return new self(
|
|
||||||
id: (string) $recipe->id->value,
|
|
||||||
seoTitle: (string) ($recipe->seoTitle ?? ''),
|
|
||||||
seoDescription: (string) ($recipe->seoDescription ?? ''),
|
|
||||||
name: $recipe->name,
|
|
||||||
picture: $picturePath,
|
|
||||||
pictureUrl: $picturePath !== null ? $publicS3Disk->publicUrlOf($picturePath) : null,
|
|
||||||
pictureAlt: '',
|
|
||||||
pictureTitle: '',
|
|
||||||
slug: $recipe->slug,
|
|
||||||
isPublished: $recipe->isPublished,
|
|
||||||
productCategory: $recipe->productRange,
|
|
||||||
consomptionTime: $recipe->consomptionTime,
|
|
||||||
inspiration: $recipe->inspiration,
|
|
||||||
season: $recipe->season,
|
|
||||||
description: $recipe->description,
|
|
||||||
details: $recipe->details,
|
|
||||||
ingredientsList: $recipe->ingredientsList,
|
|
||||||
preparationSteps: $recipe->preparationSteps,
|
|
||||||
tipsAndAdvices: $recipe->tipsAndAdvices,
|
|
||||||
createdAt: null,
|
|
||||||
updatedAt: null,
|
|
||||||
deletedAt: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function dummyForZiggy(): self
|
|
||||||
{
|
|
||||||
return new self(
|
|
||||||
id: 'dummy-id',
|
|
||||||
seoTitle: 'dummy-seo-title',
|
|
||||||
seoDescription: 'dummy-seo-description',
|
|
||||||
name: 'dummy-name',
|
|
||||||
picture: null,
|
|
||||||
pictureUrl: null,
|
|
||||||
pictureAlt: 'dummy-picture-alt',
|
|
||||||
pictureTitle: 'dummy-picture-title',
|
|
||||||
slug: 'dummy-slug',
|
|
||||||
isPublished: true,
|
|
||||||
productCategory: ProductCategory::OMELETS_AND_DELI_AND_DESSERT_LINES,
|
|
||||||
consomptionTime: ConsumptionTime::LUNCH_DINNER,
|
|
||||||
inspiration: Inspiration::FRENCH_COOKING,
|
|
||||||
season: Season::ALL_YEAR,
|
|
||||||
description: 'dummy-description',
|
|
||||||
details: 'dummy-details',
|
|
||||||
ingredientsList: 'dummy-ingredients-list',
|
|
||||||
preparationSteps: 'dummy-preparation-steps',
|
|
||||||
tipsAndAdvices: 'dummy-tips-and-advices',
|
|
||||||
createdAt: null,
|
|
||||||
updatedAt: null,
|
|
||||||
deletedAt: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getId(): RecipeId
|
|
||||||
{
|
|
||||||
return new RecipeId(Uuid::fromString($this->id));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
public function getRouteKey(): mixed
|
|
||||||
{
|
|
||||||
return $this->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
public function getRouteKeyName(): string
|
|
||||||
{
|
|
||||||
return 'id';
|
|
||||||
}
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
public function resolveRouteBinding($value, $field = null): mixed
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
public function resolveChildRouteBinding($childType, $value, $field): mixed
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function toArray(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'id' => $this->id,
|
|
||||||
'name' => $this->name,
|
|
||||||
'slug' => $this->slug,
|
|
||||||
'is_published' => $this->isPublished,
|
|
||||||
'seo_title' => $this->seoTitle,
|
|
||||||
'seo_description' => $this->seoDescription,
|
|
||||||
'picture' => $this->pictureUrl,
|
|
||||||
'picture_alt' => $this->pictureAlt,
|
|
||||||
'picture_title' => $this->pictureTitle,
|
|
||||||
'product_category' => $this->productCategory->value,
|
|
||||||
'consomption_time' => $this->consomptionTime->value,
|
|
||||||
'inspiration' => $this->inspiration->value,
|
|
||||||
'season' => $this->season->value,
|
|
||||||
'description' => $this->description,
|
|
||||||
'details' => $this->details,
|
|
||||||
'ingredients_list' => $this->ingredientsList,
|
|
||||||
'preparation_steps' => $this->preparationSteps,
|
|
||||||
'tips_and_advices' => $this->tipsAndAdvices,
|
|
||||||
'created_at' => $this->createdAt?->toISOString(),
|
|
||||||
'updated_at' => $this->updatedAt?->toISOString(),
|
|
||||||
'deleted_at' => $this->deletedAt?->toISOString(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function jsonSerialize(): array
|
|
||||||
{
|
|
||||||
return $this->toArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums\Contact;
|
|
||||||
|
|
||||||
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
|
|
||||||
|
|
||||||
enum ActivitySectorEnum: int
|
|
||||||
{
|
|
||||||
use BaseEnum;
|
|
||||||
|
|
||||||
case commercial_catering = 1;
|
|
||||||
case collective_catering = 2;
|
|
||||||
case bakery_pastry = 3;
|
|
||||||
case food_industry = 4;
|
|
||||||
case supermarkets = 5;
|
|
||||||
case others = 6;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optionnal labels definition.
|
|
||||||
*
|
|
||||||
* @phpstan-ignore classConstant.unused
|
|
||||||
*/
|
|
||||||
private const LABELS = [
|
|
||||||
self::commercial_catering->name => 'Restauration Commerciale',
|
|
||||||
self::collective_catering->name => 'Restauration Collective',
|
|
||||||
self::bakery_pastry->name => 'Boulangerie-Pâtisserie',
|
|
||||||
self::food_industry->name => 'Industrie Agro-Alimentaire',
|
|
||||||
self::supermarkets->name => 'Grandes et Moyennes Surfaces',
|
|
||||||
self::others->name => 'Autres',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Enums\Contact;
|
|
||||||
|
|
||||||
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
|
|
||||||
|
|
||||||
enum ObjectEnum: int
|
|
||||||
{
|
|
||||||
use BaseEnum;
|
|
||||||
|
|
||||||
case product_questions = 1;
|
|
||||||
case quote_request = 2;
|
|
||||||
case order_tracking = 3;
|
|
||||||
case partnership_request = 4;
|
|
||||||
case product_complaint = 5;
|
|
||||||
case other = 6;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optionnal labels definition.
|
|
||||||
*
|
|
||||||
* @phpstan-ignore classConstant.unused
|
|
||||||
*/
|
|
||||||
private const LABELS = [
|
|
||||||
self::product_questions->name => 'Questions sur nos produits',
|
|
||||||
self::quote_request->name => 'Demande de devis',
|
|
||||||
self::order_tracking->name => 'Suivi de commande',
|
|
||||||
self::partnership_request->name => 'Demande de partenariat',
|
|
||||||
self::product_complaint->name => 'Réclamation produit',
|
|
||||||
self::other->name => 'Autre',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Exceptions;
|
|
||||||
|
|
||||||
class AuthServiceException extends \RuntimeException
|
|
||||||
{
|
|
||||||
public static function of(?string $message = null, ?\Throwable $cause = null): self
|
|
||||||
{
|
|
||||||
return new self($message ?? 'An authentication error occurred.', previous: $cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Exceptions\Helpers;
|
|
||||||
|
|
||||||
class FileStorageOperationException extends \RuntimeException
|
|
||||||
{
|
|
||||||
public static function copyFailed(string $sourcePath, string $destinationPath): self
|
|
||||||
{
|
|
||||||
return new self(sprintf('Failed to copy file %s to %s', $sourcePath, $destinationPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function cannotGenerateUniqueFilename(string $folderPath, string $filename): self
|
|
||||||
{
|
|
||||||
return new self("Failed to have a unique filename for {$folderPath}/{$filename}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Exceptions\Helpers;
|
|
||||||
|
|
||||||
class UnsupportedStructureConversionException extends \RuntimeException
|
|
||||||
{
|
|
||||||
public static function fromType(mixed $input): self
|
|
||||||
{
|
|
||||||
return new self('Cannot handle $input of type ' . \gettype($input));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue