Compare commits
269 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 669ee7d736 | |||
| 01bca3a604 | |||
| 630646c47f | |||
| dea6dc8a63 | |||
| 3ea72e522f | |||
| 69e8315bd4 | |||
| 3008299cd7 | |||
| 680f0d648a | |||
| 64ca29fefc | |||
| 779fc59993 | |||
| aff271570e | |||
| db5c95f9a1 | |||
| 531a5c626a | |||
| 9bb3d1a103 | |||
| ca9c0fbc4e | |||
| 481d80c8ea | |||
| 8f9bf1e861 | |||
| b4b1dd21ac | |||
| bb454c3684 | |||
| 47671eaa6f | |||
| 19a2af8d5b | |||
| bc69c9f64d | |||
| 6c691f77fc | |||
| 04a9f97719 | |||
| 57faeada75 | |||
| da93f3e59e | |||
| 1e670eb1c2 | |||
| b67e4cff9f | |||
| 75609b7c26 | |||
| 1a09fbf269 | |||
| f21563522b | |||
| c31a2d5a08 | |||
| c3cee9910b | |||
| 39bb68bbc8 | |||
| 22e97e0bc8 | |||
| 905666e168 | |||
| 66f5d17480 | |||
| ac51908d5e | |||
| 9c5bba55c0 | |||
|
|
d54b8dda51 | ||
| 284907c2a6 | |||
| be253a969e | |||
| 522cf7a87e | |||
| e55889369b | |||
| 26723b47f4 | |||
| e8cd693cb6 | |||
| 5a65ea9414 | |||
| 81be02c717 | |||
| dee327ce56 | |||
| 4745eb493f | |||
|
|
efb4584a25 | ||
|
|
cbc98db487 | ||
|
|
c24a97345d | ||
|
|
4d50aad375 | ||
|
|
7bc4f55912 | ||
|
|
11c07644c0 | ||
|
|
1476505853 | ||
|
|
9f986d9043 | ||
| d32fd87d38 | |||
|
|
9718b292d9 | ||
| 1fa2019cc4 | |||
| f41ffc9257 | |||
|
|
6172f00a38 | ||
| ac7d180624 | |||
|
|
4bcb847d9d | ||
|
|
b7ee6e3ed6 | ||
| 3f853b8162 | |||
| 47cd01141d | |||
| 85f26b7094 | |||
| 4cdb52eb10 | |||
| 84f405d50c | |||
| f9f2456783 | |||
| 82ca13647b | |||
| 453e262664 | |||
| 03bc3a4ce3 | |||
| 089304966d | |||
| fa48faf634 | |||
| c433103588 | |||
| d6708e0749 | |||
|
|
e6084663fc | ||
| 7c80ac1f11 | |||
| dd6eeb558a | |||
| b9a0aedceb | |||
| f0ca2622d6 | |||
| a8bf4ea681 | |||
| 597e1c720a | |||
| 325c7a7fd6 | |||
| 764e58b61b | |||
| 345d0cfbfd | |||
| 7482e8cebc | |||
| cd2d8ed397 | |||
| 1d628201a6 | |||
| 93beae7ade | |||
| cad48f967c | |||
| d4e46b187c | |||
| 455ec468d8 | |||
| 6cfa8d8e27 | |||
| b989a45864 | |||
| 363f0f15e2 | |||
| 83c5ff3e6f | |||
| 26c3ff1768 | |||
| c35b65d76f | |||
| ad6d2c3413 | |||
| 48cde3db8e | |||
| ae7e63b810 | |||
| d082fd0ca1 | |||
| 16e2040d5c | |||
| 3856151908 | |||
| 4e670b04a4 | |||
| 7cdcca687d | |||
| 1cf8f31886 | |||
| 1b4052f088 | |||
| 953bf34bb3 | |||
| f86bf54242 | |||
| 7207109d63 | |||
| b2151a6e67 | |||
| c5e42e0f88 | |||
| a42b822546 | |||
| 449064d5d4 | |||
| d2c7029779 | |||
|
|
7a506c7e1a | ||
| 3331db4c6c | |||
| 1316d05b78 | |||
|
|
9efcb32ee1 | ||
|
|
9116a4bf88 | ||
|
|
5117c8e445 | ||
|
|
eb4bf9202b | ||
|
|
d641d61bfd | ||
|
|
2d1ade9d62 | ||
|
|
acdc8fe365 | ||
|
|
858711fa4f | ||
|
|
ee363196dc | ||
|
|
ccaa4de15a | ||
|
|
4b1bf703c2 | ||
|
|
aa40b8a9e4 | ||
|
|
3f5537731d | ||
|
|
6fbd8d82f4 | ||
|
|
27c3d0e87e | ||
|
|
03fdd79579 | ||
|
|
03ffaf5093 | ||
|
|
fce0f69edb | ||
|
|
a9214091c7 | ||
|
|
42514d9778 | ||
|
|
58b8c7d874 | ||
| a57b100e27 | |||
| f60ccf9909 | |||
| 1137b191c5 | |||
| 79cd52bcd6 | |||
| c21a9b334b | |||
| fd5992f65f | |||
| 36a44f0df3 | |||
| 28f447c8ec | |||
| b6d08c8bd1 | |||
| d3014b1ac8 | |||
| 2610f91bec | |||
| 5a7230ca9a | |||
| 04d038a61d | |||
| c0f5ae7a77 | |||
| 3e4201cb98 | |||
| 86b0b3cbe3 | |||
| 822d71c640 | |||
| 33494d0360 | |||
| 5a24c6c8b7 | |||
| 026b511015 | |||
| 8315650407 | |||
| 524cba1070 | |||
| 7abc85c3e3 | |||
| 108e26f9c0 | |||
| 57a9db1fd5 | |||
| 9fc2690e74 | |||
| b20846f731 | |||
| 1ad8c00241 | |||
| a1f8e410fe | |||
| 444ee46ac0 | |||
| 1a7f321cff | |||
| 1a12569395 | |||
| 1200bfea4c | |||
| e14c43d82f | |||
| 696bd9ed66 | |||
| 028f769dbc | |||
| 4139c25e42 | |||
| 213d0bb568 | |||
| 43c3d55b72 | |||
| a7992f96c8 | |||
| e39cd1ac61 | |||
| 86fdfa5560 | |||
|
|
d29100bb61 | ||
| 527a9f7664 | |||
|
|
e365d93d1f | ||
|
|
5e44470cf5 | ||
|
|
5c4670847e | ||
|
|
8b85165d42 | ||
|
|
377be050da | ||
|
|
f880a72b93 | ||
|
|
2ec086c7d7 | ||
|
|
c2eb79882e | ||
|
|
66be9ae6c0 | ||
|
|
f2a59d8356 | ||
|
|
c3c3af0b03 | ||
|
|
76edf3f575 | ||
|
|
733a406a30 | ||
|
|
c7ad4cb2ac | ||
|
|
0319478cf1 | ||
|
|
b995c2b054 | ||
|
|
af232a5fd7 | ||
|
|
0d310b0f23 | ||
|
|
a2da6b0cce | ||
|
|
fca2500680 | ||
|
|
999995dbad | ||
|
|
7a23a63792 | ||
|
|
8fd6f539fd | ||
|
|
93dc422e5b | ||
|
|
e0e5de1145 | ||
|
|
178a8be7ef | ||
|
|
8ab3701670 | ||
|
|
132b4ba5b4 | ||
|
|
64e34ecc4f | ||
|
|
7f6b1aeb3d | ||
|
|
47ce939b9e | ||
|
|
b9c8d9a7f1 | ||
|
|
92f53d2477 | ||
|
|
f03749e20c | ||
|
|
67b8a56126 | ||
|
|
54843370e2 | ||
|
|
913a49b641 | ||
|
|
d3fe410952 | ||
|
|
c2516cc295 | ||
|
|
686b965f9b | ||
|
|
d2a3b61d75 | ||
|
|
893f870735 | ||
|
|
eece6bb617 | ||
|
|
f43cb28a7d | ||
|
|
5874ea315a | ||
|
|
dea14e0491 | ||
|
|
b2e6a25a92 | ||
|
|
97e7d7e33a | ||
|
|
5bca572e67 | ||
|
|
264be08046 | ||
|
|
c4fd2b048e | ||
|
|
120db17596 | ||
|
|
c747288704 | ||
|
|
324e796fc6 | ||
|
|
4512798e44 | ||
|
|
379ffe003f | ||
|
|
fc472db322 | ||
|
|
672af76d4b | ||
|
|
ecae3660d0 | ||
|
|
4eca9c7d24 | ||
|
|
cdc7e86682 | ||
|
|
cb9d7ac0d5 | ||
|
|
4421d8e770 | ||
|
|
cc2c137b26 | ||
|
|
05af5cf83c | ||
|
|
083e89364b | ||
|
|
9aa84edcf3 | ||
|
|
76a42c17f6 | ||
|
|
af13d298fd | ||
|
|
433edf13c8 | ||
|
|
2f90c82d74 | ||
|
|
6a82a59bc5 | ||
|
|
db25bec3e5 | ||
|
|
f42dc37674 | ||
|
|
f2a4a367e0 | ||
|
|
a7d30e8353 | ||
|
|
9a404dd61a | ||
|
|
32c6a9391d | ||
|
|
2dd2c78fa7 | ||
|
|
eef93adb2c | ||
|
|
a6370dcb47 |
738 changed files with 118157 additions and 2017 deletions
24
.editorconfig
Normal file
24
.editorconfig
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
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
Normal file
79
.env.example
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
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
|
||||||
4
.eslintignore
Normal file
4
.eslintignore
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
node_modules
|
||||||
|
public
|
||||||
|
vendor
|
||||||
|
resources/ts/components/ui/
|
||||||
80
.eslintrc.cjs
Normal file
80
.eslintrc.cjs
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
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
Normal file
11
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
* 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
Normal file
40
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
*.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,35 +1,221 @@
|
||||||
.docker-build:
|
stages:
|
||||||
stage: build
|
- install
|
||||||
image:
|
- lint
|
||||||
name: gcr.io/kaniko-project/executor:v1.23.2-debug
|
- test
|
||||||
entrypoint: [ "" ]
|
- security
|
||||||
script:
|
- build
|
||||||
- /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
|
- deploy
|
||||||
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: 'deploy/deploy.yml'
|
file: "security/composer.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"
|
||||||
|
|
||||||
stages:
|
# * LINT --------------------------------------------------------------------
|
||||||
- build
|
|
||||||
- deploy
|
.php-lint-base: &php-lint-base
|
||||||
|
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: []
|
||||||
|
|
|
||||||
11
.gitlab/issue_templates/bug.md
Normal file
11
.gitlab/issue_templates/bug.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
## Comportement constaté
|
||||||
|
|
||||||
|
|
||||||
|
## Comportement attendu
|
||||||
|
|
||||||
|
|
||||||
|
## Solution envisagée
|
||||||
|
|
||||||
|
|
||||||
|
## Étapes pour la reproduction du bug
|
||||||
|
*
|
||||||
7
.gitlab/issue_templates/feature.md
Normal file
7
.gitlab/issue_templates/feature.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
## Présentation du besoin
|
||||||
|
|
||||||
|
|
||||||
|
## Implémentation
|
||||||
|
|
||||||
|
|
||||||
|
## Maquettes, documentations...
|
||||||
2
.husky/commit-msg
Executable file
2
.husky/commit-msg
Executable file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
npx commitlint --config node_modules/@uwil/husky-config/commitlint.config.js --edit $1
|
||||||
56
.stylelintrc.json
Normal file
56
.stylelintrc.json
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
"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
Normal file
5
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"emeraldwalk.runonsave"
|
||||||
|
]
|
||||||
|
}
|
||||||
109
.vscode/settings.json
vendored
Normal file
109
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
{
|
||||||
|
"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
Normal file
12
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# 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
|
||||||
53
Dockerfile
53
Dockerfile
|
|
@ -1,4 +1,55 @@
|
||||||
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/ldcweb-production-8.3:170625-101047
|
ARG PHP_8_5_TAG=latest
|
||||||
|
|
||||||
|
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,111 +1,33 @@
|
||||||
# OVO TEAM
|
# Table of contents
|
||||||
|
|
||||||
## Instalation
|
- [Table of contents](#table-of-contents)
|
||||||
|
- [Frameworks, Platforms and Libraries](#frameworks-platforms-and-libraries)
|
||||||
|
- [Documentation](#documentation)
|
||||||
|
- [Changelog](#changelog)
|
||||||
|
- [Copyright and License](#copyright-and-license)
|
||||||
|
|
||||||
Copier `.env` vers `.env.example` et remplir les informations.
|
# Frameworks, Platforms and Libraries
|
||||||
|
|
||||||
## Déployer
|
<!-- https://github.com/Ileriayo/markdown-badges -->
|
||||||
|
|
||||||
- Apres git pull sur l'envirronnement désiré
|
[](https://sass-lang.com/)
|
||||||
- Envoyer les éléments uploads si nécéssaire (Ne devrais pas étre le cas)
|
[](https://tailwindcss.com/)
|
||||||
- Executer la partie `Compiler le front`
|
[](https://vite.dev/)
|
||||||
|
[](https://www.php.net/)
|
||||||
|
[](https://laravel.com/docs/master)
|
||||||
|
|
||||||
## Faire une mise à jour
|
# Documentation
|
||||||
|
|
||||||
Commenter dans `public/.htaccess` la partie `# Hide update.php`, puis accéder à ce fichier via la navigateur
|
```php
|
||||||
|
composer i/npm i
|
||||||
|
```
|
||||||
|
|
||||||
## Fichier uploads
|
# Changelog
|
||||||
|
|
||||||
Au sein de `public/sites/default/files`
|
Latest version v1.0.0.
|
||||||
|
|
||||||
Les dossiers :
|
See the [CHANGELOG.md](CHANGELOG.md) file for details.
|
||||||
|
|
||||||
- `inline-images` Pour les contenus insérés dans les WYSIWYG
|
# Copyright and License
|
||||||
- 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
|
|
||||||
|
|
||||||
## Compiler le front
|
Copyright (c) 2025 and beyond UWIL.
|
||||||
|
|
||||||
`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
Normal file
32
RGPD.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# 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
Normal file
31184
_ide_helper.php
Normal file
File diff suppressed because it is too large
Load diff
45
app/Casts/FilePathCast.php
Normal file
45
app/Casts/FilePathCast.php
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
136
app/Casts/NutritionalValuesCast.php
Normal file
136
app/Casts/NutritionalValuesCast.php
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
app/Contracts/Action.php
Normal file
18
app/Contracts/Action.php
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
9
app/Contracts/ActionInput.php
Normal file
9
app/Contracts/ActionInput.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Contracts;
|
||||||
|
|
||||||
|
interface ActionInput
|
||||||
|
{
|
||||||
|
}
|
||||||
13
app/Contracts/ActionInputEmpty.php
Normal file
13
app/Contracts/ActionInputEmpty.php
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Contracts;
|
||||||
|
|
||||||
|
final class ActionInputEmpty implements ActionInput
|
||||||
|
{
|
||||||
|
public static function create(): self
|
||||||
|
{
|
||||||
|
return new self();
|
||||||
|
}
|
||||||
|
}
|
||||||
53
app/Contracts/Directories/ProductDirectory.php
Normal file
53
app/Contracts/Directories/ProductDirectory.php
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?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;
|
||||||
|
}
|
||||||
39
app/Contracts/Directories/RecipeDirectory.php
Normal file
39
app/Contracts/Directories/RecipeDirectory.php
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?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;
|
||||||
|
}
|
||||||
15
app/Contracts/Directory.php
Normal file
15
app/Contracts/Directory.php
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Contracts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template TEntity
|
||||||
|
* @template TId
|
||||||
|
*
|
||||||
|
* @method TEntity|null find(TId $id)
|
||||||
|
*/
|
||||||
|
interface Directory
|
||||||
|
{
|
||||||
|
}
|
||||||
19
app/Contracts/Executor.php
Normal file
19
app/Contracts/Executor.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?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;
|
||||||
|
}
|
||||||
18
app/Contracts/Query.php
Normal file
18
app/Contracts/Query.php
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
9
app/Contracts/QueryInput.php
Normal file
9
app/Contracts/QueryInput.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Contracts;
|
||||||
|
|
||||||
|
interface QueryInput
|
||||||
|
{
|
||||||
|
}
|
||||||
13
app/Contracts/QueryInputEmpty.php
Normal file
13
app/Contracts/QueryInputEmpty.php
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Contracts;
|
||||||
|
|
||||||
|
final class QueryInputEmpty implements QueryInput
|
||||||
|
{
|
||||||
|
public static function create(): self
|
||||||
|
{
|
||||||
|
return new self();
|
||||||
|
}
|
||||||
|
}
|
||||||
17
app/Contracts/Repository.php
Normal file
17
app/Contracts/Repository.php
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?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
|
||||||
|
{
|
||||||
|
}
|
||||||
59
app/Cqs/Actions/Products/Create/CreateProduct.php
Normal file
59
app/Cqs/Actions/Products/Create/CreateProduct.php
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?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));
|
||||||
|
}
|
||||||
|
}
|
||||||
77
app/Cqs/Actions/Products/Create/CreateProductInput.php
Normal file
77
app/Cqs/Actions/Products/Create/CreateProductInput.php
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
<?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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Cqs\Actions\Products\Create;
|
||||||
|
|
||||||
|
final readonly class FailedToCreateProduct
|
||||||
|
{
|
||||||
|
}
|
||||||
37
app/Cqs/Actions/Products/Delete/DeleteProduct.php
Normal file
37
app/Cqs/Actions/Products/Delete/DeleteProduct.php
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?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));
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/Cqs/Actions/Products/Delete/DeleteProductInput.php
Normal file
16
app/Cqs/Actions/Products/Delete/DeleteProductInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Cqs\Actions\Products\Delete;
|
||||||
|
|
||||||
|
final readonly class FailedToDeleteProduct
|
||||||
|
{
|
||||||
|
}
|
||||||
112
app/Cqs/Actions/Products/Duplicate/DuplicateProduct.php
Normal file
112
app/Cqs/Actions/Products/Duplicate/DuplicateProduct.php
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/Cqs/Actions/Products/Duplicate/DuplicateProductInput.php
Normal file
16
app/Cqs/Actions/Products/Duplicate/DuplicateProductInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Cqs\Actions\Products\Duplicate;
|
||||||
|
|
||||||
|
final readonly class FailedToDuplicateProduct
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Cqs\Actions\Products\ForceDelete;
|
||||||
|
|
||||||
|
final readonly class FailedToForceDeleteProduct
|
||||||
|
{
|
||||||
|
}
|
||||||
55
app/Cqs/Actions/Products/ForceDelete/ForceDeleteProduct.php
Normal file
55
app/Cqs/Actions/Products/ForceDelete/ForceDeleteProduct.php
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Cqs\Actions\Products\Restore;
|
||||||
|
|
||||||
|
final readonly class FailedToRestoreProduct
|
||||||
|
{
|
||||||
|
}
|
||||||
37
app/Cqs/Actions/Products/Restore/RestoreProduct.php
Normal file
37
app/Cqs/Actions/Products/Restore/RestoreProduct.php
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?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));
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/Cqs/Actions/Products/Restore/RestoreProductInput.php
Normal file
16
app/Cqs/Actions/Products/Restore/RestoreProductInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Cqs\Actions\Products\Update;
|
||||||
|
|
||||||
|
final readonly class FailedToUpdateProduct
|
||||||
|
{
|
||||||
|
}
|
||||||
84
app/Cqs/Actions/Products/Update/UpdateProduct.php
Normal file
84
app/Cqs/Actions/Products/Update/UpdateProduct.php
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
79
app/Cqs/Actions/Products/Update/UpdateProductInput.php
Normal file
79
app/Cqs/Actions/Products/Update/UpdateProductInput.php
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
app/Cqs/Actions/Recipes/Create/CreateRecipe.php
Normal file
56
app/Cqs/Actions/Recipes/Create/CreateRecipe.php
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?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));
|
||||||
|
}
|
||||||
|
}
|
||||||
53
app/Cqs/Actions/Recipes/Create/CreateRecipeInput.php
Normal file
53
app/Cqs/Actions/Recipes/Create/CreateRecipeInput.php
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
app/Cqs/Actions/Recipes/Create/FailedToCreateRecipe.php
Normal file
9
app/Cqs/Actions/Recipes/Create/FailedToCreateRecipe.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Cqs\Actions\Recipes\Create;
|
||||||
|
|
||||||
|
final readonly class FailedToCreateRecipe
|
||||||
|
{
|
||||||
|
}
|
||||||
37
app/Cqs/Actions/Recipes/Delete/DeleteRecipe.php
Normal file
37
app/Cqs/Actions/Recipes/Delete/DeleteRecipe.php
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?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));
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/Cqs/Actions/Recipes/Delete/DeleteRecipeInput.php
Normal file
16
app/Cqs/Actions/Recipes/Delete/DeleteRecipeInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
9
app/Cqs/Actions/Recipes/Delete/FailedToDeleteRecipe.php
Normal file
9
app/Cqs/Actions/Recipes/Delete/FailedToDeleteRecipe.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Cqs\Actions\Recipes\Delete;
|
||||||
|
|
||||||
|
final readonly class FailedToDeleteRecipe
|
||||||
|
{
|
||||||
|
}
|
||||||
116
app/Cqs/Actions/Recipes/Duplicate/DuplicateRecipe.php
Normal file
116
app/Cqs/Actions/Recipes/Duplicate/DuplicateRecipe.php
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/Cqs/Actions/Recipes/Duplicate/DuplicateRecipeInput.php
Normal file
16
app/Cqs/Actions/Recipes/Duplicate/DuplicateRecipeInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Cqs\Actions\Recipes\Duplicate;
|
||||||
|
|
||||||
|
final readonly class FailedToDuplicateRecipe
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Cqs\Actions\Recipes\ForceDelete;
|
||||||
|
|
||||||
|
final readonly class FailedToForceDeleteRecipe
|
||||||
|
{
|
||||||
|
}
|
||||||
50
app/Cqs/Actions/Recipes/ForceDelete/ForceDeleteRecipe.php
Normal file
50
app/Cqs/Actions/Recipes/ForceDelete/ForceDeleteRecipe.php
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Cqs\Actions\Recipes\Restore;
|
||||||
|
|
||||||
|
final readonly class FailedToRestoreRecipe
|
||||||
|
{
|
||||||
|
}
|
||||||
37
app/Cqs/Actions/Recipes/Restore/RestoreRecipe.php
Normal file
37
app/Cqs/Actions/Recipes/Restore/RestoreRecipe.php
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?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));
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/Cqs/Actions/Recipes/Restore/RestoreRecipeInput.php
Normal file
16
app/Cqs/Actions/Recipes/Restore/RestoreRecipeInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
9
app/Cqs/Actions/Recipes/Update/FailedToUpdateRecipe.php
Normal file
9
app/Cqs/Actions/Recipes/Update/FailedToUpdateRecipe.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Cqs\Actions\Recipes\Update;
|
||||||
|
|
||||||
|
final readonly class FailedToUpdateRecipe
|
||||||
|
{
|
||||||
|
}
|
||||||
75
app/Cqs/Actions/Recipes/Update/UpdateRecipe.php
Normal file
75
app/Cqs/Actions/Recipes/Update/UpdateRecipe.php
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
app/Cqs/Actions/Recipes/Update/UpdateRecipeInput.php
Normal file
55
app/Cqs/Actions/Recipes/Update/UpdateRecipeInput.php
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
app/Cqs/Queries/Products/FindProductByIdInput.php
Normal file
17
app/Cqs/Queries/Products/FindProductByIdInput.php
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?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
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/Cqs/Queries/Products/FindProductBySlugInput.php
Normal file
16
app/Cqs/Queries/Products/FindProductBySlugInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?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
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
54
app/Cqs/Queries/Products/FindProductQuery.php
Normal file
54
app/Cqs/Queries/Products/FindProductQuery.php
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
app/Cqs/Queries/Products/GetBoPaginatedProductsQuery.php
Normal file
80
app/Cqs/Queries/Products/GetBoPaginatedProductsQuery.php
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
59
app/Cqs/Queries/Products/GetCategoriesProductsQuery.php
Normal file
59
app/Cqs/Queries/Products/GetCategoriesProductsQuery.php
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/Cqs/Queries/Products/GetCategoriesProductsQueryInput.php
Normal file
36
app/Cqs/Queries/Products/GetCategoriesProductsQueryInput.php
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
app/Cqs/Queries/Products/GetProductsSummariesQuery.php
Normal file
51
app/Cqs/Queries/Products/GetProductsSummariesQuery.php
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/Cqs/Queries/Products/GetRelatedProductsQuery.php
Normal file
43
app/Cqs/Queries/Products/GetRelatedProductsQuery.php
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Cqs/Queries/Products/GetRelatedProductsQueryInput.php
Normal file
19
app/Cqs/Queries/Products/GetRelatedProductsQueryInput.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
app/Cqs/Queries/Recipes/FindRecipeByIdInput.php
Normal file
17
app/Cqs/Queries/Recipes/FindRecipeByIdInput.php
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?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
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/Cqs/Queries/Recipes/FindRecipeBySlugInput.php
Normal file
16
app/Cqs/Queries/Recipes/FindRecipeBySlugInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?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
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
54
app/Cqs/Queries/Recipes/FindRecipeQuery.php
Normal file
54
app/Cqs/Queries/Recipes/FindRecipeQuery.php
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
85
app/Cqs/Queries/Recipes/GetBoPaginatedRecipesQuery.php
Normal file
85
app/Cqs/Queries/Recipes/GetBoPaginatedRecipesQuery.php
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
<?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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/Cqs/Queries/Recipes/GetBoPaginatedRecipesQueryInput.php
Normal file
21
app/Cqs/Queries/Recipes/GetBoPaginatedRecipesQueryInput.php
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/Cqs/Queries/Recipes/GetBoPaginatedRecipesQueryOutput.php
Normal file
28
app/Cqs/Queries/Recipes/GetBoPaginatedRecipesQueryOutput.php
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?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,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
70
app/Cqs/Queries/Recipes/GetFrontRecipesQuery.php
Normal file
70
app/Cqs/Queries/Recipes/GetFrontRecipesQuery.php
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
app/Cqs/Queries/Recipes/GetFrontRecipesQueryInput.php
Normal file
34
app/Cqs/Queries/Recipes/GetFrontRecipesQueryInput.php
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/Cqs/Queries/Recipes/GetRelatedRecipesQuery.php
Normal file
43
app/Cqs/Queries/Recipes/GetRelatedRecipesQuery.php
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Cqs/Queries/Recipes/GetRelatedRecipesQueryInput.php
Normal file
19
app/Cqs/Queries/Recipes/GetRelatedRecipesQueryInput.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
228
app/Directories/EloquentProductDirectory.php
Normal file
228
app/Directories/EloquentProductDirectory.php
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
143
app/Directories/EloquentRecipeDirectory.php
Normal file
143
app/Directories/EloquentRecipeDirectory.php
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
59
app/Dto/CategoryProductsDto.php
Normal file
59
app/Dto/CategoryProductsDto.php
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?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,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
242
app/Dto/ProductDto.php
Normal file
242
app/Dto/ProductDto.php
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
15
app/Dto/ProductSummaryDto.php
Normal file
15
app/Dto/ProductSummaryDto.php
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Dto;
|
||||||
|
|
||||||
|
final readonly class ProductSummaryDto
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $id,
|
||||||
|
public string $name,
|
||||||
|
public ?string $pictureUrl,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
208
app/Dto/RecipeDto.php
Normal file
208
app/Dto/RecipeDto.php
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/Enums/Contact/ActivitySectorEnum.php
Normal file
33
app/Enums/Contact/ActivitySectorEnum.php
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?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',
|
||||||
|
];
|
||||||
|
}
|
||||||
33
app/Enums/Contact/ObjectEnum.php
Normal file
33
app/Enums/Contact/ObjectEnum.php
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?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',
|
||||||
|
];
|
||||||
|
}
|
||||||
13
app/Exceptions/AuthServiceException.php
Normal file
13
app/Exceptions/AuthServiceException.php
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
app/Exceptions/Helpers/FileStorageOperationException.php
Normal file
18
app/Exceptions/Helpers/FileStorageOperationException.php
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?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));
|
||||||
|
}
|
||||||
|
}
|
||||||
13
app/Exceptions/ImageOptimizerException.php
Normal file
13
app/Exceptions/ImageOptimizerException.php
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
class ImageOptimizerException extends \RuntimeException
|
||||||
|
{
|
||||||
|
public static function of(?string $message = null, ?\Throwable $cause = null): self
|
||||||
|
{
|
||||||
|
return new self($message ?? 'An image optimization error occurred.', previous: $cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue