Compare commits
188 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | ||
|
|
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 |
672 changed files with 114009 additions and 2012 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
|
||||
75
.env.example
Normal file
75
.env.example
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
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=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
SCOUT_DRIVER=meilisearch
|
||||
MEILISEARCH_HOST=
|
||||
MEILISEARCH_KEY=
|
||||
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
|
||||
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
*.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
|
||||
|
||||
public/favicon-back
|
||||
/docker-compose.yml
|
||||
250
.gitlab-ci.yml
250
.gitlab-ci.yml
|
|
@ -1,35 +1,221 @@
|
|||
.docker-build:
|
||||
stage: build
|
||||
image:
|
||||
name: gcr.io/kaniko-project/executor:v1.23.2-debug
|
||||
entrypoint: [ "" ]
|
||||
script:
|
||||
- /kaniko/executor --context "${CI_PROJECT_DIR}" --build-arg "GITLAB_USER=${CI_DEPLOY_USER}" --build-arg "GITLAB_PASSWORD=${CI_DEPLOY_PASSWORD}" --dockerfile "${CI_PROJECT_DIR}/Dockerfile" --destination "${IMAGE_TAG}" --cache=true --cache-ttl 24h
|
||||
allow_failure: false
|
||||
|
||||
docker-build:staging:
|
||||
extends: .docker-build
|
||||
variables:
|
||||
IMAGE_TAG: "${CI_REGISTRY_IMAGE}:s-${CI_COMMIT_SHA}"
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "trunk"
|
||||
when: manual
|
||||
allow_failure: true
|
||||
|
||||
docker-build:latest:
|
||||
extends: .docker-build
|
||||
variables:
|
||||
IMAGE_TAG: "${CI_REGISTRY_IMAGE}:p-${CI_COMMIT_SHA}"
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master"
|
||||
when: manual
|
||||
allow_failure: true
|
||||
stages:
|
||||
- install
|
||||
- lint
|
||||
- test
|
||||
- security
|
||||
- build
|
||||
- deploy
|
||||
|
||||
include:
|
||||
- project: 'ldc-web/shared-ci'
|
||||
ref: 'main'
|
||||
file: 'deploy/deploy.yml'
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
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:
|
||||
- build
|
||||
- deploy
|
||||
# * LINT --------------------------------------------------------------------
|
||||
|
||||
.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
|
||||
43
DOMAIN.md
Normal file
43
DOMAIN.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
## Domaine Métier et Contraintes : Les Produits
|
||||
|
||||
Le catalogue de produits repose sur une segmentation précise nécessitant obligatoirement 5 critères d'identification[cite: 1, 2]. Les produits se déclinent sous de très nombreux formats et conditionnements (colis, barquettes, bidons, poches, sachets, seaux, verrines, etc.)[cite: 2].
|
||||
|
||||
### Caractéristiques et filtres des produits
|
||||
|
||||
| Attribut | Valeurs métier définies | Contraintes de sélection |
|
||||
| :--- | :--- | :--- |
|
||||
| **Catégories / Gammes** | Œufs liquides petit conditionnement, Œufs liquides industrie, Œufs liquides pour la grande distribution, Œufs durs, Œufs brouillés, Œufs pochés, Omelettes et Gammes traiteur et desserts[cite: 1, 2]. | 1 seule par produit[cite: 1, 2]. |
|
||||
| **Technologies** | Frais, Surgelé, Ambiant[cite: 2]. | 1 seule par produit[cite: 2]. |
|
||||
| **Modes d'élevage** | Plein air, Bio, Sol, Conventionnel et Label Rouge[cite: 1, 2]. | Minimum 1, maximum illimité (X)[cite: 1]. |
|
||||
| **Métiers (Cibles)** | Hôtellerie, Restauration rapide et snacking, Boulangerie pâtisserie, Restauration collective, Industrie agro-alimentaire, Grande distribution et Restauration à table[cite: 1, 2]. | Minimum 1, maximum illimité (X)[cite: 1]. |
|
||||
| **Égalim** | Oui, Non[cite: 2]. | Obligatoire ("Oui" ou "Non")[cite: 2]. |
|
||||
|
||||
### Contraintes d'affichage (Fiche Produit)
|
||||
|
||||
* Les champs dédiés aux valeurs nutritionnelles et le fichier PDF ne doivent pas s'afficher en front-office si aucune information n'est complétée[cite: 1].
|
||||
* Une section « Consignes de conservation avant ouverture » doit être ajoutée en dessous de la section détails/avantages[cite: 1].
|
||||
* Une section « Consignes de conservation après ouverture » doit être ajoutée sous la section "avant ouverture", mais ne doit pas s'afficher si elle n'est pas complétée[cite: 1].
|
||||
* Un slider de recettes doit être implémenté en bas de page avec 9 slides[cite: 1].
|
||||
* Ce slider doit afficher en premier les recettes associées au produit, puis compléter avec d'autres recettes de manière aléatoire[cite: 1].
|
||||
* Aucune liaison avec INCO n'est à prévoir (pas de code INCO)[cite: 1].
|
||||
|
||||
---
|
||||
|
||||
## Domaine Métier et Contraintes : Les Recettes
|
||||
|
||||
Les recettes interagissent avec les produits et sont soumises à 4 filtres spécifiques pour faciliter la navigation des utilisateurs[cite: 2].
|
||||
|
||||
### Caractéristiques et filtres des recettes
|
||||
|
||||
| Attribut | Valeurs métier définies | Contraintes de sélection |
|
||||
| :--- | :--- | :--- |
|
||||
| **Gammes de produits** | Identiques aux gammes de produits : Œufs liquides (petit conditionnement, industrie, grande distribution), Œufs durs, brouillés, pochés, Omelettes, traiteur et desserts[cite: 1, 2]. | 1 seule gamme par recette[cite: 1]. |
|
||||
| **Moments de consommation** | Petit déjeuner / brunch, Déjeuner / dîner, Snacking, Dessert / pâtisserie[cite: 1, 2]. | 1 seul moment par recette[cite: 1]. |
|
||||
| **Inspirations** | Cuisine du monde, cuisine française, cuisine asiatique, cuisine protéinée, Snacking, Sucré, Brunch[cite: 1, 2]. | 1 seule inspiration par recette[cite: 1]. |
|
||||
| **Saisons** | Printemps/été, Automne/hiver, Toute l'année[cite: 1, 2]. | 1 seule saison par recette[cite: 1]. |
|
||||
|
||||
### Contraintes d'affichage (Fiche Recette)
|
||||
|
||||
* Le champ « astuces et conseils » ne doit pas s'afficher en front-office si aucune information n'est complétée[cite: 1].
|
||||
* Un slider des produits doit être implémenté en bas de page avec 6 slides[cite: 1].
|
||||
* Ce slider doit afficher en premier les produits associés à la recette, puis afficher d'autres produits en aléatoire pour compléter[cite: 1].
|
||||
51
Dockerfile
51
Dockerfile
|
|
@ -1,5 +1,54 @@
|
|||
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 node:24-alpine AS frontend
|
||||
|
||||
ARG GITLAB_PASSWORD
|
||||
|
||||
RUN mkdir -p /app/public/build
|
||||
|
||||
COPY package*.json postcss.config.js tailwind.config.js tsconfig.json vite.config.ts .eslintrc.json .stylelintrc.json /app/
|
||||
COPY resources/ /app/resources/
|
||||
COPY --from=vendor /app/vendor/ /app/vendor/
|
||||
WORKDIR /app
|
||||
RUN npm config set @ldc-web:registry https://lab.s-web.fr/api/v4/packages/npm/
|
||||
RUN npm config set -- '//lab.s-web.fr/api/v4/packages/npm/:_authToken' "${GITLAB_PASSWORD}"
|
||||
RUN npm config set @uwil:registry https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/
|
||||
RUN npm config set -- '//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken' "${GITLAB_PASSWORD}"
|
||||
|
||||
RUN npm install --no-audit --no-fund --loglevel=error && npm 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
|
||||
|
||||
COPY --chown=${USER}:${USER} . /app/srcs
|
||||
USER ${USER}
|
||||
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}
|
||||
|
|
|
|||
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é
|
||||
- Envoyer les éléments uploads si nécéssaire (Ne devrais pas étre le cas)
|
||||
- Executer la partie `Compiler le front`
|
||||
[](https://sass-lang.com/)
|
||||
[](https://tailwindcss.com/)
|
||||
[](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
|
||||
- les dossiers `20*` comme `2023-05` par exemple pour les media uploadés via la création de pages
|
||||
- `styles` qui contienent des images uploadés de dimension spécifiques
|
||||
# Copyright and License
|
||||
|
||||
## Compiler le front
|
||||
|
||||
`node_modules/gulp/bin/gulp.js -f public/themes/custom/custtheme/gulpfile.js`
|
||||
|
||||
Deactiver `Aggreger les fichiers CSS et javascript` au sein de `/fr/admin/config/development/performance`
|
||||
|
||||
Cliquer sur `Effacer tous les caches` si vous reactivez l'option
|
||||
|
||||
---
|
||||
|
||||
## Original README
|
||||
|
||||
Drupal is an open source content management platform supporting a variety of
|
||||
websites ranging from personal weblogs to large community-driven websites. For
|
||||
more information, visit the Drupal website, [Drupal.org][Drupal.org], and join
|
||||
the [Drupal community][Drupal community].
|
||||
|
||||
## Contributing
|
||||
|
||||
Drupal is developed on [Drupal.org][Drupal.org], the home of the international
|
||||
Drupal community since 2001!
|
||||
|
||||
[Drupal.org][Drupal.org] hosts Drupal's [GitLab repository][GitLab repository],
|
||||
its [issue queue][issue queue], and its [documentation][documentation]. Before
|
||||
you start working on code, be sure to search the [issue queue][issue queue] and
|
||||
create an issue if your aren't able to find an existing issue.
|
||||
|
||||
Every issue on Drupal.org automatically creates a new community-accessible fork
|
||||
that you can contribute to. Learn more about the code contribution process on
|
||||
the [Issue forks & merge requests page][issue forks].
|
||||
|
||||
## Usage
|
||||
|
||||
For a brief introduction, see [USAGE.txt](/core/USAGE.txt). You can also find
|
||||
guides, API references, and more by visiting Drupal's [documentation
|
||||
page][documentation].
|
||||
|
||||
You can quickly extend Drupal's core feature set by installing any of its
|
||||
[thousands of free and open source modules][modules]. With Drupal and its
|
||||
module ecosystem, you can often build most or all of what your project needs
|
||||
before writing a single line of code.
|
||||
|
||||
## Changelog
|
||||
|
||||
Drupal keeps detailed [change records][changelog]. You can search Drupal's
|
||||
changes for a record of every notable breaking change and new feature since
|
||||
2011.
|
||||
|
||||
## Security
|
||||
|
||||
For a list of security announcements, see the [Security advisories
|
||||
page][Security advisories] (available as [an RSS feed][security RSS]). This
|
||||
page also describes how to subscribe to these announcements via email.
|
||||
|
||||
For information about the Drupal security process, or to find out how to report
|
||||
a potential security issue to the Drupal security team, see the [Security team
|
||||
page][security team].
|
||||
|
||||
## Need a helping hand?
|
||||
|
||||
Visit the [Support page][support] or browse [over a thousand Drupal
|
||||
providers][service providers] offering design, strategy, development, and
|
||||
hosting services.
|
||||
|
||||
## Legal matters
|
||||
|
||||
Know your rights when using Drupal by reading Drupal core's
|
||||
[license](/core/LICENSE.txt).
|
||||
|
||||
Learn about the [Drupal trademark and logo policy here][trademark].
|
||||
|
||||
[Drupal.org]: https://www.drupal.org
|
||||
[Drupal community]: https://www.drupal.org/community
|
||||
[GitLab repository]: https://git.drupalcode.org/project/drupal
|
||||
[issue queue]: https://www.drupal.org/project/issues/drupal
|
||||
[issue forks]: https://www.drupal.org/drupalorg/docs/gitlab-integration/issue-forks-merge-requests
|
||||
[documentation]: https://www.drupal.org/documentation
|
||||
[changelog]: https://www.drupal.org/list-changes/drupal
|
||||
[modules]: https://www.drupal.org/project/project_module
|
||||
[security advisories]: https://www.drupal.org/security
|
||||
[security RSS]: https://www.drupal.org/security/rss.xml
|
||||
[security team]: https://www.drupal.org/drupal-security-team
|
||||
[service providers]: https://www.drupal.org/drupal-services
|
||||
[support]: https://www.drupal.org/support
|
||||
[trademark]: https://www.drupal.com/trademark
|
||||
Copyright (c) 2025 and beyond UWIL.
|
||||
|
|
|
|||
31103
_ide_helper.php
Normal file
31103
_ide_helper.php
Normal file
File diff suppressed because it is too large
Load diff
122
app/Casts/NutritionalValuesCast.php
Normal file
122
app/Casts/NutritionalValuesCast.php
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<?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;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/** @implements CastsAttributes<NutritionalValues|null, NutritionalValues|array<string, mixed>|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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = is_string($value) ? json_decode($value, true) : $value;
|
||||
|
||||
if (! is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->fromArray($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function set(Model $model, string $key, mixed $value, array $attributes): array
|
||||
{
|
||||
if ($value === null) {
|
||||
return [$key => null];
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$value = json_decode($value, true);
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$value = $this->fromArray($value);
|
||||
}
|
||||
|
||||
if (! $value instanceof NutritionalValues) {
|
||||
throw new InvalidArgumentException('The given value is not a NutritionalValues instance.');
|
||||
}
|
||||
|
||||
return [$key => json_encode($value->toArray())];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $data
|
||||
*/
|
||||
private function fromArray(array $data): ?NutritionalValues
|
||||
{
|
||||
$refWeightData = Arr::array($data, 'refWeightG');
|
||||
|
||||
$rawVal = Arr::get($refWeightData, 'value');
|
||||
$rawUnit = Arr::get($refWeightData, 'unit');
|
||||
|
||||
if ((! is_string($rawVal) && ! is_numeric($rawVal)) || ! is_string($rawUnit)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$refWeightUnit = NutritionnalUnit::tryFrom($rawUnit);
|
||||
if ($refWeightUnit === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @param array<string|int, mixed> $data */
|
||||
$getProperty = function (array $data, string $key): ?NutritionalValue {
|
||||
return when(
|
||||
is_array(Arr::get($data, $key)),
|
||||
fn () => $this->toNutritionalValues(Arr::array($data, $key)),
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
return new NutritionalValues(
|
||||
refWeightG: new NutritionalValue((string) $rawVal, $refWeightUnit),
|
||||
kj: $getProperty($data, 'kj'),
|
||||
kcal: $getProperty($data, 'kcal'),
|
||||
fatG: $getProperty($data, 'fatG'),
|
||||
satFatG: $getProperty($data, 'satFatG'),
|
||||
carbG: $getProperty($data, 'carbG'),
|
||||
sugarG: $getProperty($data, 'sugarG'),
|
||||
fiberG: $getProperty($data, 'fiberG'),
|
||||
proteinG: $getProperty($data, 'proteinG'),
|
||||
saltG: $getProperty($data, 'saltG'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string|int, mixed> $data
|
||||
*/
|
||||
private function toNutritionalValues(array $data): ?NutritionalValue
|
||||
{
|
||||
$rawItemVal = Arr::get($data, 'value');
|
||||
$rawItemUnit = Arr::get($data, 'unit');
|
||||
|
||||
if ((! is_string($rawItemVal) && ! is_numeric($rawItemVal)) || ! is_string($rawItemUnit)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = mb_trim((string) $rawItemVal);
|
||||
$unit = NutritionnalUnit::tryFrom($rawItemUnit);
|
||||
|
||||
if ($value === '' || $unit === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new NutritionalValue($value, $unit);
|
||||
}
|
||||
}
|
||||
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\Dtos\CategoryProducts;
|
||||
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, \App\Models\Product> */
|
||||
interface ProductDirectory extends Directory
|
||||
{
|
||||
public function find(ProductId $id): ?Product;
|
||||
|
||||
public function findBySlug(string $slug): ?Product;
|
||||
|
||||
/**
|
||||
* @return \App\Dtos\ProductSummary[]
|
||||
*/
|
||||
public function getAllProductsSummary(): 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
|
||||
): ?CategoryProducts;
|
||||
|
||||
/** @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, \App\Models\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
|
||||
{
|
||||
}
|
||||
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
|
||||
{
|
||||
}
|
||||
224
app/Directories/EloquentProductDirectory.php
Normal file
224
app/Directories/EloquentProductDirectory.php
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Directories;
|
||||
|
||||
use App\Contracts\Directories\ProductDirectory;
|
||||
use App\Dtos\CategoryProducts;
|
||||
use App\Dtos\ProductSummary;
|
||||
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 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 getAllProductsSummary(): 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
|
||||
): ?CategoryProducts {
|
||||
|
||||
$dbQueryCallback = function (Builder $query) use ($category, $breedingMethods, $technologies, $businesses, $isEgalim) {
|
||||
/** @var \Illuminate\Database\Eloquent\Builder<Product> $query */
|
||||
$query->where('category', $category->value)
|
||||
->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 CategoryProducts(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
pictureAlt: $product->picture_alt,
|
||||
pictureTitle: $product->picture_title,
|
||||
pdfFilePath: $product->pdf,
|
||||
associatedRecipes: $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()
|
||||
->with(['recipes'])
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(fn (Product $product) => $this->mapToEntity($product))
|
||||
->all();
|
||||
}
|
||||
}
|
||||
141
app/Directories/EloquentRecipeDirectory.php
Normal file
141
app/Directories/EloquentRecipeDirectory.php
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
<?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']);
|
||||
|
||||
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,
|
||||
picture: $recipe->picture,
|
||||
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()
|
||||
->with(['products'])
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(fn (Recipe $recipe) => $this->mapToEntity($recipe))
|
||||
->all();
|
||||
}
|
||||
}
|
||||
49
app/Dtos/CategoryProducts.php
Normal file
49
app/Dtos/CategoryProducts.php
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Dtos;
|
||||
|
||||
use Domain\Models\Entities\Product;
|
||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
||||
use Illuminate\Support\Arr;
|
||||
use Override;
|
||||
|
||||
final readonly class CategoryProducts 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
|
||||
{
|
||||
return array_merge(
|
||||
Arr::wrap(json_decode(json_encode($product) ?: '', true)),
|
||||
[
|
||||
'id' => (string) $product->id->value,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
15
app/Dtos/ProductSummary.php
Normal file
15
app/Dtos/ProductSummary.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Dtos;
|
||||
|
||||
final readonly class ProductSummary
|
||||
{
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $name,
|
||||
public ?string $picture,
|
||||
) {
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
87
app/Http/Controllers/Back/BackendController.php
Normal file
87
app/Http/Controllers/Back/BackendController.php
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Back;
|
||||
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Contracts\Session\Session;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Str;
|
||||
use Uwil\LaravelBackend\Controllers\BackendController as UwilBackendController;
|
||||
use Uwil\LaravelBackend\Enums\ItemsPerPaginationEnum;
|
||||
use Uwil\LaravelCommon\Helpers\ToolboxHelper;
|
||||
|
||||
class BackendController extends UwilBackendController
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
use ValidatesRequests;
|
||||
|
||||
public function __construct(
|
||||
protected readonly Request $request,
|
||||
protected readonly Session $session,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize pagination with cache or config (default).
|
||||
*
|
||||
* @template T of \Illuminate\Database\Eloquent\Model
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder<T> $query
|
||||
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator<array-key,T>
|
||||
*/
|
||||
protected function paginate(
|
||||
Builder $query,
|
||||
?ItemsPerPaginationEnum $pagination = null
|
||||
): LengthAwarePaginator {
|
||||
$route = $this->request->route();
|
||||
|
||||
throw_if(
|
||||
$route === null,
|
||||
\UnexpectedValueException::class,
|
||||
'Route is required in order to paginate'
|
||||
);
|
||||
|
||||
$cacheKey = $this->getCacheKey($route);
|
||||
$perPage = $this->getPerPage($cacheKey);
|
||||
|
||||
$this->persistPerPage($cacheKey, $perPage);
|
||||
|
||||
return $query->paginate(perPage: $perPage)->withQueryString();
|
||||
}
|
||||
|
||||
private function persistPerPage(string $cacheKey, int $perPage): void
|
||||
{
|
||||
if ($this->session->get($cacheKey) !== $perPage) {
|
||||
$this->session->put($cacheKey, $perPage);
|
||||
}
|
||||
}
|
||||
|
||||
private function getPerPage(string $cacheKey): int
|
||||
{
|
||||
$perPage = $this->session->get($cacheKey);
|
||||
$perPage = $perPage ?? ItemsPerPaginationEnum::twelve->value();
|
||||
|
||||
if (! is_scalar($perPage)) {
|
||||
throw new \UnexpectedValueException('perPage shall be a scalar valuye');
|
||||
}
|
||||
|
||||
return ToolboxHelper::getValidatedEnum(
|
||||
intval($perPage),
|
||||
'pagination',
|
||||
'\Uwil\LaravelBackend\Enums\ItemsPerPaginationEnum',
|
||||
);
|
||||
}
|
||||
|
||||
private function getCacheKey(Route $route): string
|
||||
{
|
||||
$currentRoutePath = Str::slug($route->getName() ?? 'unknown');
|
||||
|
||||
return "pagination.{$currentRoutePath}";
|
||||
}
|
||||
}
|
||||
240
app/Http/Controllers/Back/ProductController.php
Normal file
240
app/Http/Controllers/Back/ProductController.php
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Back;
|
||||
|
||||
use App\Http\Requests\Back\Products\IndexProductRequest;
|
||||
use App\Http\Requests\Back\Products\StoreProductRequest;
|
||||
use App\Http\Requests\Back\Products\UpdateProductRequest;
|
||||
use App\Http\Resources\CommonResource;
|
||||
use App\Models\Product;
|
||||
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 Illuminate\Contracts\Session\Session;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Response;
|
||||
|
||||
class ProductController extends BackendController
|
||||
{
|
||||
public function __construct(
|
||||
Request $request,
|
||||
Session $session,
|
||||
) {
|
||||
parent::__construct(
|
||||
$request,
|
||||
$session
|
||||
);
|
||||
|
||||
$this->modelName = Product::modelName();
|
||||
$this->modelPage = Product::modelPage();
|
||||
}
|
||||
|
||||
public function index(IndexProductRequest $request): Response
|
||||
{
|
||||
$this->authorize('viewAny', Product::class);
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Builder<\App\Models\Product> $query */
|
||||
$query = Product::query();
|
||||
|
||||
if ($request->routeIs('back.products.trash')) {
|
||||
$query = $query->onlyTrashed();
|
||||
}
|
||||
|
||||
$searchValue = $request->getSearchValue();
|
||||
if ($searchValue) {
|
||||
$this->searchQuery(
|
||||
$query,
|
||||
$searchValue,
|
||||
null,
|
||||
'name'
|
||||
);
|
||||
}
|
||||
$searchFields = \implode(', ', [
|
||||
trans('validation.attributes.name'),
|
||||
]);
|
||||
|
||||
$this->sortQuery($query);
|
||||
|
||||
$paginatedModels = $this->paginate($query);
|
||||
|
||||
return $this->inertiaRender('back/Pages/Products/Index', [
|
||||
'paginatedModels' => CommonResource::collection($paginatedModels),
|
||||
'search' => $searchValue,
|
||||
'searchFields' => $searchFields,
|
||||
'enumProductCategory' => ProductCategory::toArray(),
|
||||
'enumTechnology' => Technology::toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Product $product): RedirectResponse
|
||||
{
|
||||
$this->authorize('viewAny', Product::class);
|
||||
|
||||
return redirect()->route('back.products.edit', $product);
|
||||
}
|
||||
|
||||
public function create(Product $product): Response
|
||||
{
|
||||
$this->authorize('create', Product::class);
|
||||
|
||||
return $this->inertiaRender('back/Pages/Products/Create', [
|
||||
'data' => CommonResource::make($product),
|
||||
'enumProductCategory' => ProductCategory::toArray(),
|
||||
'enumTechnology' => Technology::toArray(),
|
||||
'enumBreedingMethod' => BreedingMethod::toArray(),
|
||||
'enumBusiness' => Business::toArray(),
|
||||
'enumProductFormat' => ProductFormat::toArray(),
|
||||
'enumNutritionnalUnit' => NutritionnalUnit::toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreProductRequest $request): RedirectResponse
|
||||
{
|
||||
$this->authorize('create', Product::class);
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$product = new Product();
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = $request->only($product->getFillable());
|
||||
$product->fill($data);
|
||||
|
||||
if ($product->saveOrFail()) {
|
||||
return redirect()->route('back.products.edit', $product)
|
||||
->with('success', trans('uwil-backend::crud.messages.has_been_created', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.product', 1)),
|
||||
]));
|
||||
}
|
||||
|
||||
return redirect()->back()
|
||||
->with('error', trans('uwil-backend::crud.messages.cannot_be_created', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.product', 1)),
|
||||
]));
|
||||
});
|
||||
}
|
||||
|
||||
public function edit(Product $product): Response
|
||||
{
|
||||
$this->authorize('update', $product);
|
||||
|
||||
return $this->inertiaRender('back/Pages/Products/Edit', [
|
||||
'data' => CommonResource::make($product),
|
||||
'enumProductCategory' => ProductCategory::toArray(),
|
||||
'enumTechnology' => Technology::toArray(),
|
||||
'enumBreedingMethod' => BreedingMethod::toArray(),
|
||||
'enumBusiness' => Business::toArray(),
|
||||
'enumProductFormat' => ProductFormat::toArray(),
|
||||
'enumNutritionnalUnit' => NutritionnalUnit::toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(
|
||||
UpdateProductRequest $request,
|
||||
Product $product
|
||||
): RedirectResponse {
|
||||
$this->authorize('update', $product);
|
||||
|
||||
return DB::transaction(function () use ($request, $product) {
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = $request->only($product->getFillable());
|
||||
$product->fill($data);
|
||||
|
||||
if ($product->saveOrFail()) {
|
||||
return redirect()->route('back.products.edit', $product)
|
||||
->with('success', trans('uwil-backend::crud.messages.has_been_updated', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.product', 1)),
|
||||
]));
|
||||
}
|
||||
|
||||
return redirect()->route('back.products.edit', $product)
|
||||
->with('error', trans('uwil-backend::crud.messages.cannot_be_updated', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.product', 1)),
|
||||
]));
|
||||
});
|
||||
}
|
||||
|
||||
public function duplicate(Product $product): Response
|
||||
{
|
||||
return $this->create($product->replicate());
|
||||
}
|
||||
|
||||
public function restore(string $id): RedirectResponse
|
||||
{
|
||||
$product = Product::withTrashed()->findOrFail($id);
|
||||
$this->authorize('restore', $product);
|
||||
|
||||
$previousQueries = $this->getPreviousQueries();
|
||||
|
||||
if ($product->restore()) {
|
||||
return redirect()->route('back.products.trash', $previousQueries)
|
||||
->with('success', trans('uwil-backend::crud.messages.has_been_restored', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.product', 1)),
|
||||
]));
|
||||
}
|
||||
|
||||
return redirect()->back()
|
||||
->with('error', trans('uwil-backend::crud.messages.cannot_be_restored', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.product', 1)),
|
||||
]));
|
||||
}
|
||||
|
||||
public function destroy(Product $product): RedirectResponse
|
||||
{
|
||||
$this->authorize('delete', $product);
|
||||
$previousQueries = $this->getPreviousQueries();
|
||||
|
||||
if ($product->delete()) {
|
||||
return redirect()->route('back.products.index', $previousQueries)
|
||||
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.product', 1)),
|
||||
]));
|
||||
}
|
||||
|
||||
return redirect()->back()
|
||||
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.product', 1)),
|
||||
]));
|
||||
}
|
||||
|
||||
public function forceDestroy(string $id): RedirectResponse
|
||||
{
|
||||
$product = Product::withTrashed()->findOrFail($id);
|
||||
$this->authorize('forceDelete', $product);
|
||||
|
||||
$previousQueries = $this->getPreviousQueries();
|
||||
|
||||
if ($product->forceDelete()) {
|
||||
return redirect()->route('back.products.trash', $previousQueries)
|
||||
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.product', 1)),
|
||||
]));
|
||||
}
|
||||
|
||||
return redirect()->back()
|
||||
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.product', 1)),
|
||||
]));
|
||||
}
|
||||
|
||||
/** @return array<array-key,string|array<mixed>> */
|
||||
private function getPreviousQueries(): array
|
||||
{
|
||||
$previousQueries = [];
|
||||
$previousQueryString = \parse_url($this->getPreviousUrl(), \PHP_URL_QUERY) ?? '' ?: '';
|
||||
\parse_str($previousQueryString, $previousQueries);
|
||||
|
||||
return $previousQueries;
|
||||
}
|
||||
|
||||
private function getPreviousUrl(): string
|
||||
{
|
||||
return url()->previous(route('back.dashboard.index'));
|
||||
}
|
||||
}
|
||||
251
app/Http/Controllers/Back/RecipeController.php
Normal file
251
app/Http/Controllers/Back/RecipeController.php
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Back;
|
||||
|
||||
use App\Contracts\Directories\ProductDirectory;
|
||||
use App\Http\Requests\Back\Recipes\IndexRecipeRequest;
|
||||
use App\Http\Requests\Back\Recipes\StoreRecipeRequest;
|
||||
use App\Http\Requests\Back\Recipes\UpdateRecipeRequest;
|
||||
use App\Http\Resources\CommonResource;
|
||||
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\Contracts\Session\Session;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Response;
|
||||
|
||||
class RecipeController extends BackendController
|
||||
{
|
||||
public function __construct(
|
||||
Request $request,
|
||||
Session $session,
|
||||
private readonly ProductDirectory $productDirectory,
|
||||
) {
|
||||
parent::__construct(
|
||||
$request,
|
||||
$session
|
||||
);
|
||||
|
||||
$this->modelName = Recipe::modelName();
|
||||
$this->modelPage = Recipe::modelPage();
|
||||
}
|
||||
|
||||
public function index(IndexRecipeRequest $request): Response
|
||||
{
|
||||
$this->authorize('viewAny', Recipe::class);
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Builder<\App\Models\Recipe> $query */
|
||||
$query = Recipe::query();
|
||||
|
||||
if ($request->routeIs('back.recipes.trash')) {
|
||||
$query = $query->onlyTrashed();
|
||||
}
|
||||
|
||||
$searchValue = $request->getSearchValue();
|
||||
if ($searchValue) {
|
||||
$this->searchQuery(
|
||||
$query,
|
||||
$searchValue,
|
||||
null,
|
||||
'name'
|
||||
);
|
||||
}
|
||||
$searchFields = \implode(', ', [
|
||||
trans('validation.attributes.name'),
|
||||
]);
|
||||
|
||||
$this->sortQuery($query);
|
||||
|
||||
$paginatedModels = $this->paginate($query);
|
||||
|
||||
return $this->inertiaRender('back/Pages/Recipes/Index', [
|
||||
'paginatedModels' => CommonResource::collection($paginatedModels),
|
||||
'search' => $searchValue,
|
||||
'searchFields' => $searchFields,
|
||||
'enumProductCategory' => ProductCategory::toArray(),
|
||||
'enumConsumptionTime' => ConsumptionTime::toArray(),
|
||||
'enumInspiration' => Inspiration::toArray(),
|
||||
'enumSeason' => Season::toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Recipe $recipe): RedirectResponse
|
||||
{
|
||||
$this->authorize('viewAny', Recipe::class);
|
||||
|
||||
return redirect()->route('back.recipes.edit', $recipe);
|
||||
}
|
||||
|
||||
public function create(Recipe $recipe): Response
|
||||
{
|
||||
$this->authorize('create', Recipe::class);
|
||||
|
||||
return $this->inertiaRender('back/Pages/Recipes/Create', [
|
||||
'data' => CommonResource::make($recipe),
|
||||
'enumProductCategory' => ProductCategory::toArray(),
|
||||
'enumConsumptionTime' => ConsumptionTime::toArray(),
|
||||
'enumInspiration' => Inspiration::toArray(),
|
||||
'enumSeason' => Season::toArray(),
|
||||
'allProducts' => $this->productDirectory->getAllProductsSummary(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreRecipeRequest $request): RedirectResponse
|
||||
{
|
||||
$this->authorize('create', Recipe::class);
|
||||
|
||||
return DB::transaction(function () use ($request) {
|
||||
$recipe = new Recipe();
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = $request->only($recipe->getFillable());
|
||||
$recipe->fill($data);
|
||||
|
||||
if ($recipe->saveOrFail()) {
|
||||
$recipe->products()
|
||||
->sync($request->array('products'));
|
||||
|
||||
return redirect()->route('back.recipes.edit', $recipe)
|
||||
->with('success', trans('uwil-backend::crud.messages.has_been_created', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.recipes', 1)),
|
||||
]));
|
||||
}
|
||||
|
||||
return redirect()->back()
|
||||
->with('error', trans('uwil-backend::crud.messages.cannot_be_created', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.recipe', 1)),
|
||||
]));
|
||||
});
|
||||
}
|
||||
|
||||
public function edit(Recipe $recipe): Response
|
||||
{
|
||||
$this->authorize('update', $recipe);
|
||||
|
||||
$recipe->load('products');
|
||||
|
||||
return $this->inertiaRender('back/Pages/Recipes/Edit', [
|
||||
'data' => CommonResource::make($recipe),
|
||||
'enumProductCategory' => ProductCategory::toArray(),
|
||||
'enumConsumptionTime' => ConsumptionTime::toArray(),
|
||||
'enumInspiration' => Inspiration::toArray(),
|
||||
'enumSeason' => Season::toArray(),
|
||||
'allProducts' => $this->productDirectory->getAllProductsSummary(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(
|
||||
UpdateRecipeRequest $request,
|
||||
Recipe $recipe
|
||||
): RedirectResponse {
|
||||
$this->authorize('update', $recipe);
|
||||
|
||||
return DB::transaction(function () use ($request, $recipe) {
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = $request->only($recipe->getFillable());
|
||||
$recipe->fill($data);
|
||||
|
||||
if ($recipe->saveOrFail()) {
|
||||
$recipe->products()
|
||||
->sync($request->array('products'));
|
||||
|
||||
return redirect()->route('back.recipes.edit', $recipe)
|
||||
->with('success', trans('uwil-backend::crud.messages.has_been_updated', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.recipe', 1)),
|
||||
]));
|
||||
}
|
||||
|
||||
return redirect()->route('back.recipes.edit', $recipe)
|
||||
->with('error', trans('uwil-backend::crud.messages.cannot_be_updated', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.recipe', 1)),
|
||||
]));
|
||||
});
|
||||
}
|
||||
|
||||
public function duplicate(Recipe $recipe): Response
|
||||
{
|
||||
$this->authorize('duplicate', $recipe);
|
||||
|
||||
return $this->create($recipe->replicate());
|
||||
}
|
||||
|
||||
public function restore(string $id): RedirectResponse
|
||||
{
|
||||
$recipe = Recipe::withTrashed()->findOrFail($id);
|
||||
$this->authorize('restore', $recipe);
|
||||
|
||||
$previousQueries = $this->getPreviousQueries();
|
||||
|
||||
if ($recipe->restore()) {
|
||||
return redirect()->route('back.recipes.trash', $previousQueries)
|
||||
->with('success', trans('uwil-backend::crud.messages.has_been_restored', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.recipe', 1)),
|
||||
]));
|
||||
}
|
||||
|
||||
return redirect()->back()
|
||||
->with('error', trans('uwil-backend::crud.messages.cannot_be_restored', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.recipe', 1)),
|
||||
]));
|
||||
}
|
||||
|
||||
public function destroy(Recipe $recipe): RedirectResponse
|
||||
{
|
||||
$this->authorize('delete', $recipe);
|
||||
|
||||
$previousQueries = $this->getPreviousQueries();
|
||||
|
||||
if ($recipe->delete()) {
|
||||
return redirect()->route('back.recipes.index', $previousQueries)
|
||||
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.recipe', 1)),
|
||||
]));
|
||||
}
|
||||
|
||||
return redirect()->back()
|
||||
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.recipe', 1)),
|
||||
]));
|
||||
}
|
||||
|
||||
public function forceDestroy(string $id): RedirectResponse
|
||||
{
|
||||
$recipe = Recipe::withTrashed()->findOrFail($id);
|
||||
$this->authorize('forceDelete', $recipe);
|
||||
|
||||
$previousQueries = $this->getPreviousQueries();
|
||||
|
||||
if ($recipe->forceDelete()) {
|
||||
return redirect()->route('back.recipes.trash', $previousQueries)
|
||||
->with('success', trans('uwil-backend::crud.messages.has_been_deleted', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.recipe', 1)),
|
||||
]));
|
||||
}
|
||||
|
||||
return redirect()->back()
|
||||
->with('error', trans('uwil-backend::crud.messages.cannot_be_deleted', [
|
||||
'model' => Str::ucfirst(trans_choice('models.classes.recipe', 1)),
|
||||
]));
|
||||
}
|
||||
|
||||
/** @return array<array-key,string|array<mixed>> */
|
||||
private function getPreviousQueries(): array
|
||||
{
|
||||
$previousQueries = [];
|
||||
$previousQueryString = \parse_url($this->getPreviousUrl(), \PHP_URL_QUERY) ?? '' ?: '';
|
||||
\parse_str($previousQueryString, $previousQueries);
|
||||
|
||||
return $previousQueries;
|
||||
}
|
||||
|
||||
private function getPreviousUrl(): string
|
||||
{
|
||||
return url()->previous(route('back.dashboard.index'));
|
||||
}
|
||||
}
|
||||
17
app/Http/Controllers/Controller.php
Normal file
17
app/Http/Controllers/Controller.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
use DispatchesJobs;
|
||||
use ValidatesRequests;
|
||||
}
|
||||
75
app/Http/Controllers/Front/ProductController.php
Normal file
75
app/Http/Controllers/Front/ProductController.php
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Front;
|
||||
|
||||
use App\Contracts\Directories\ProductDirectory;
|
||||
use App\Contracts\Directories\RecipeDirectory;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Front\Products\IndexProductRequest;
|
||||
use Domain\Models\Entities\Product;
|
||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ProductController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProductDirectory $productDirectory,
|
||||
private readonly RecipeDirectory $recipeDirectory,
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(IndexProductRequest $request): Response
|
||||
{
|
||||
$categories = $request->getCategories();
|
||||
|
||||
$breedingMethods = $request->getBreedingMethods();
|
||||
$technologies = $request->getTechnologies();
|
||||
$businesses = $request->getBusinesses();
|
||||
$isEgalim = $request->getIsEgalim();
|
||||
$search = $request->getSearchValue() ?? '';
|
||||
|
||||
$categoriesProducts = [];
|
||||
|
||||
if (count($categories) === 0) {
|
||||
$categories = ! $request->hasFilter() ? [
|
||||
ProductCategory::OMELETS_AND_DELI_AND_DESSERT_LINES,
|
||||
ProductCategory::HARD_BOILED_EGGS,
|
||||
] : ProductCategory::cases();
|
||||
}
|
||||
|
||||
foreach ($categories as $category) {
|
||||
$result = $this->productDirectory->getCategoryProducts(
|
||||
category: $category,
|
||||
breedingMethods: $breedingMethods,
|
||||
technologies: $technologies,
|
||||
businesses: $businesses,
|
||||
isEgalim: $isEgalim,
|
||||
search: $search,
|
||||
// * No limit on search is volontary, limit is done in front, the creative team did not mind the pagination work.
|
||||
limitPerCategory: -1
|
||||
);
|
||||
|
||||
if ($result !== null) {
|
||||
$categoriesProducts[] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
return Inertia::render('front/Pages/Products/Index', [
|
||||
'categoriesProducts' => $categoriesProducts,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Product $product): Response
|
||||
{
|
||||
$relatedRecipes = $this->recipeDirectory
|
||||
->getRelatedRecipes($product->id(), 10);
|
||||
|
||||
return Inertia::render('front/Pages/Products/Show', [
|
||||
'product' => $product,
|
||||
'relatedRecipes' => $relatedRecipes,
|
||||
]);
|
||||
}
|
||||
}
|
||||
79
app/Http/Controllers/Front/RecipeController.php
Normal file
79
app/Http/Controllers/Front/RecipeController.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Front;
|
||||
|
||||
use App\Contracts\Directories\ProductDirectory;
|
||||
use App\Contracts\Directories\RecipeDirectory;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Front\Recipes\IndexRecipeRequest;
|
||||
use Domain\Models\Entities\Recipe;
|
||||
use Domain\Models\ValueObjects\Pagination;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class RecipeController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RecipeDirectory $recipeDirectory,
|
||||
private readonly ProductDirectory $productDirectory,
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(IndexRecipeRequest $request): Response
|
||||
{
|
||||
$categories = $request->getCategories();
|
||||
$consumptionTimes = $request->getConsumptionTimes();
|
||||
$inspirations = $request->getInspirations();
|
||||
$seasons = $request->getSeasons();
|
||||
$search = $request->getSearchValue() ?? '';
|
||||
|
||||
// * Load all recipes for client-side display
|
||||
$allRecipes = [];
|
||||
$page = 1;
|
||||
$perPage = 100;
|
||||
|
||||
do {
|
||||
$pagination = Pagination::ofPage($page, $perPage);
|
||||
|
||||
$paginatedRecipes = $this->recipeDirectory->getPaginatedRecipes(
|
||||
pagination: $pagination,
|
||||
productRanges: $categories,
|
||||
consumptionTimes: $consumptionTimes,
|
||||
inspirations: $inspirations,
|
||||
seasons: $seasons,
|
||||
search: $search
|
||||
);
|
||||
|
||||
foreach ($paginatedRecipes->items() as $recipe) {
|
||||
$allRecipes[] = $recipe;
|
||||
}
|
||||
|
||||
$page++;
|
||||
} while ($page <= $paginatedRecipes->lastPage());
|
||||
|
||||
$mockedPaginatedRecipes = new LengthAwarePaginator(
|
||||
items: $allRecipes,
|
||||
total: count($allRecipes),
|
||||
perPage: max(1, count($allRecipes)),
|
||||
currentPage: 1,
|
||||
);
|
||||
|
||||
return Inertia::render('front/Pages/Recipes/Index', [
|
||||
'paginatedRecipes' => $mockedPaginatedRecipes,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Recipe $recipe): Response
|
||||
{
|
||||
$relatedProducts = $this->productDirectory
|
||||
->getRelatedProducts($recipe->id(), 10);
|
||||
|
||||
return Inertia::render('front/Pages/Recipes/Show', [
|
||||
'recipe' => $recipe,
|
||||
'relatedProducts' => $relatedProducts,
|
||||
]);
|
||||
}
|
||||
}
|
||||
96
app/Http/Middlewares/HtmlSanitizer.php
Normal file
96
app/Http/Middlewares/HtmlSanitizer.php
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middlewares;
|
||||
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use Illuminate\Http\Request;
|
||||
use Mews\Purifier\Purifier;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class HtmlSanitizer
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Purifier $purifierService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request):(\Illuminate\Http\Response) $next
|
||||
* @param string ...$params List of input names that should be sanitized with rich sanitization (allowing some HTML)
|
||||
*/
|
||||
public function handle(Request $request, \Closure $next, string ...$params): Response
|
||||
{
|
||||
$inputs = $request->all();
|
||||
\array_walk($inputs, function (mixed &$input, int | string $name) use ($params): void {
|
||||
$this->cleanInput($input, (string) $name, null, $params);
|
||||
});
|
||||
$request->merge($inputs);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $params
|
||||
*/
|
||||
private function cleanInput(mixed &$input, string $name, ?string $rootName, array $params): void
|
||||
{
|
||||
if (is_array($input)) {
|
||||
array_walk_recursive($input, function (mixed &$nestedInput, int | string $nestedName) use ($name, $params): void {
|
||||
$this->cleanInput($nestedInput, (string) $nestedName, $name, $params);
|
||||
});
|
||||
}
|
||||
|
||||
if (! is_string($input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->shouldApplyRichSanitization($name, $rootName, $params)) {
|
||||
$this->sanitizeRichInput($input);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$input = strip_tags($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $params
|
||||
*/
|
||||
private function shouldApplyRichSanitization(string $name, ?string $rootName, array $params): bool
|
||||
{
|
||||
return in_array($name, $params) || in_array($rootName ?? $name, $params);
|
||||
}
|
||||
|
||||
private function sanitizeRichInput(string &$input): void
|
||||
{
|
||||
$isJson = \json_decode($input, true);
|
||||
if ($isJson && is_array($isJson)) {
|
||||
\array_walk_recursive($isJson, function (string | array &$item): void {
|
||||
if (\is_string($item)) {
|
||||
$item = $this->purifyString($item);
|
||||
}
|
||||
});
|
||||
|
||||
$input = json_encode($isJson, \JSON_THROW_ON_ERROR) ?: throw new \UnexpectedValueException('Failed to encode JSON after sanitization.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$input = $this->purifyString($input);
|
||||
}
|
||||
|
||||
private function purifyString(string $input): string
|
||||
{
|
||||
$cleaned = $this->purifierService->clean($input);
|
||||
|
||||
if (! is_string($cleaned)) {
|
||||
throw new \UnexpectedValueException('Purifier should return a string when input is a string.');
|
||||
}
|
||||
|
||||
return ToolboxHelper::stripCkEditorSchemesOnMediaInputs($cleaned);
|
||||
}
|
||||
}
|
||||
173
app/Http/Middlewares/SecureHeaders.php
Normal file
173
app/Http/Middlewares/SecureHeaders.php
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middlewares;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Vite;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SecureHeaders
|
||||
{
|
||||
/**
|
||||
* Enumerate headers which you do not want in your application's responses.
|
||||
* Great starting point would be to go check out @Scott_Helme's:
|
||||
* https://securityheaders.com/
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $unwantedHeaderList = [
|
||||
'X-Powered-By',
|
||||
'Server',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly Container $container
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request):(\Illuminate\Http\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$appUrl = Config::string('app.url');
|
||||
$appDebug = Config::boolean('app.debug');
|
||||
$telescopeEnabled = Config::boolean('telescope.enabled', false);
|
||||
|
||||
// * For Vite DEV
|
||||
$host = \parse_url($appUrl, \PHP_URL_HOST);
|
||||
$viteDevOrigin = $appDebug && \is_string($host) && $host !== ''
|
||||
? "https://{$host}:4443"
|
||||
: '';
|
||||
$viteDevSocket = $appDebug && \is_string($host) && $host !== ''
|
||||
? "wss://{$host}:4443"
|
||||
: '';
|
||||
|
||||
$nonce = Vite::cspNonce();
|
||||
$jsBo = $request->routeIs('bo*') ? "'unsafe-eval'" : '';
|
||||
$jsdelivrUrl = 'https://cdn.jsdelivr.net';
|
||||
$openStreetMapTiles = 'https://*.tile.openstreetmap.org';
|
||||
|
||||
$this->declareNonce($nonce);
|
||||
$this->removeUnwantedHeaders($this->unwantedHeaderList);
|
||||
|
||||
$response = $next($request);
|
||||
|
||||
$response->headers->set('Referrer-Policy', 'no-referrer-when-downgrade');
|
||||
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
||||
$response->headers->set('X-XSS-Protection', '1; mode=block');
|
||||
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
|
||||
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
|
||||
|
||||
// * Ignore CSP on some conditions.
|
||||
if (
|
||||
$appDebug && (
|
||||
$response->isClientError() || // Ignition
|
||||
$response->isServerError() || // Ignition
|
||||
Vite::isRunningHot() || // Vite
|
||||
($telescopeEnabled && str_starts_with($request->path(), 'telescope')) // Telescope
|
||||
)
|
||||
) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$cspPolicies = [
|
||||
"default-src 'none';",
|
||||
"base-uri 'self';",
|
||||
"manifest-src 'self';",
|
||||
"media-src 'self' blob: {$appUrl};",
|
||||
// * CKEDITOR Specific https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/csp.html
|
||||
"connect-src 'self' blob: " . implode(' ', [
|
||||
'https://yl.s-web.fr/',
|
||||
// * BAN address autocomplete (api-adresse.data.gouv.fr)
|
||||
'https://api-adresse.data.gouv.fr',
|
||||
'https://maps.googleapis.com',
|
||||
// * Google reCAPTCHA
|
||||
'https://www.google.com/recaptcha/',
|
||||
'https://recaptcha.google.com/recaptcha/',
|
||||
'https://www.google.com',
|
||||
'https://www.recaptcha.net/',
|
||||
// * VITE DEV
|
||||
$viteDevSocket,
|
||||
$viteDevOrigin,
|
||||
]) . ';',
|
||||
"img-src 'self' data: blob: " . implode(' ', [
|
||||
'https://maps.googleapis.com',
|
||||
'https://maps.gstatic.com',
|
||||
$viteDevOrigin,
|
||||
$openStreetMapTiles,
|
||||
]) . ' data:;',
|
||||
'child-src ' . implode(' ', []) . ';',
|
||||
"frame-src 'self' " . implode(' ', [
|
||||
'https://www.google.com/recaptcha/',
|
||||
'https://recaptcha.google.com/recaptcha/',
|
||||
'https://www.google.com/maps/',
|
||||
'https://www.youtube.com/embed/',
|
||||
'https://player.vimeo.com/',
|
||||
'https://e.issuu.com/embed.html?',
|
||||
]) . ';',
|
||||
"object-src 'none';",
|
||||
"font-src 'self' data: " . \implode(' ', [
|
||||
'https://fonts.gstatic.com',
|
||||
// * VITE DEV
|
||||
$viteDevOrigin,
|
||||
]) . ';',
|
||||
// ! Using CKEditor will must have unsafe-inline style
|
||||
"style-src 'self' 'unsafe-inline' " . \implode(' ', [
|
||||
$jsdelivrUrl,
|
||||
'https://cdnjs.cloudflare.com',
|
||||
'https://fonts.googleapis.com',
|
||||
'https://use.fontawesome.com',
|
||||
// * File Manager
|
||||
// * VITE DEV
|
||||
$viteDevOrigin,
|
||||
]) . ';',
|
||||
|
||||
// ! If you dont use ckeditor use this
|
||||
// "style-src 'self' 'nonce-$nonce';",
|
||||
"script-src 'self' 'nonce-$nonce' 'strict-dynamic' " . \implode(' ', [
|
||||
'https: blob:',
|
||||
$jsBo,
|
||||
'https://www.googletagmanager.com',
|
||||
'https://cdnjs.cloudflare.com',
|
||||
'https://www.google-analytics.com',
|
||||
// * VITE DEV
|
||||
$viteDevOrigin,
|
||||
]) . ';',
|
||||
];
|
||||
|
||||
$response->headers->set(
|
||||
'Content-Security-Policy',
|
||||
implode(' ', $cspPolicies)
|
||||
);
|
||||
$response->headers->set('Feature-Policy', 'fullscreen \'self\'; geolocation \'self\';');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove unwanted headers
|
||||
*
|
||||
* @param string[] $headerList List of headers to remove from the response
|
||||
* @return void
|
||||
*/
|
||||
private function removeUnwantedHeaders(array $headerList)
|
||||
{
|
||||
foreach ($headerList as $header) {
|
||||
header_remove($header);
|
||||
}
|
||||
}
|
||||
|
||||
private function declareNonce(?string $value): void
|
||||
{
|
||||
Vite::useCspNonce($value);
|
||||
$this->container->singleton('csp_nonce', $value);
|
||||
}
|
||||
}
|
||||
39
app/Http/Requests/Back/Products/IndexProductRequest.php
Normal file
39
app/Http/Requests/Back/Products/IndexProductRequest.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Back\Products;
|
||||
|
||||
use App\Services\AdminAuthService;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class IndexProductRequest extends FormRequest
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AdminAuthService $authService
|
||||
) {
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->authService->check();
|
||||
}
|
||||
|
||||
public function getSearchValue(): ?string
|
||||
{
|
||||
return $this->has('search') ?
|
||||
$this->string('search')->value() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Validation\Rule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'search' => ['sometimes', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
133
app/Http/Requests/Back/Products/StoreProductRequest.php
Normal file
133
app/Http/Requests/Back/Products/StoreProductRequest.php
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Back\Products;
|
||||
|
||||
use App\Services\AdminAuthService;
|
||||
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 Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Rules\Enum;
|
||||
|
||||
class StoreProductRequest extends FormRequest
|
||||
{
|
||||
protected const int MAX_FILE_SIZE_KB = 5120;
|
||||
|
||||
protected const int PICTURE_WIDTH = 800;
|
||||
|
||||
protected const int PICTURE_HEIGHT = 800;
|
||||
|
||||
public function __construct(
|
||||
private readonly AdminAuthService $authService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->authService->check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Validation\Rule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'seo_title' => ['required', 'string', 'between:45,70'],
|
||||
'seo_description' => ['required', 'string', 'between:50,160'],
|
||||
'category' => ['required', new Enum(ProductCategory::class)],
|
||||
'technology' => ['required', new Enum(Technology::class)],
|
||||
'is_egalim' => ['boolean'],
|
||||
'breeding_methods' => ['required', 'array'],
|
||||
'breeding_methods.*' => ['required', new Enum(BreedingMethod::class)],
|
||||
'businesses' => ['required', 'array'],
|
||||
'businesses.*' => ['required', new Enum(Business::class)],
|
||||
'formats' => ['required', 'array'],
|
||||
'formats.*' => ['required', new Enum(ProductFormat::class)],
|
||||
'description' => ['required', 'string', 'min:15', 'max:500'],
|
||||
'details_and_advantages' => ['required', 'string'],
|
||||
'handling_advices' => ['required', 'string'],
|
||||
'ingredients' => ['nullable', 'string'],
|
||||
'conservation_before_opening' => ['required', 'string', 'max:255'],
|
||||
'conservation_after_opening' => ['nullable', 'string', 'max:255'],
|
||||
'picture' => [
|
||||
'required',
|
||||
Rule::imageFile()
|
||||
->max(self::MAX_FILE_SIZE_KB)
|
||||
->dimensions(
|
||||
Rule::dimensions()
|
||||
->width(self::PICTURE_WIDTH)
|
||||
->height(self::PICTURE_HEIGHT)
|
||||
),
|
||||
],
|
||||
'picture_alt' => ['required', 'string', 'min:4', 'max:75'],
|
||||
'picture_title' => ['required', 'string', 'min:4', 'max:75'],
|
||||
'pdf' => [
|
||||
'nullable',
|
||||
Rule::file()
|
||||
->max(self::MAX_FILE_SIZE_KB)
|
||||
->types('pdf'),
|
||||
],
|
||||
'nutritional_values' => ['nullable', 'array'],
|
||||
'nutritional_values.*.value' => ['required_with:nutritional_values.*.unit', 'string', 'min:1', 'max:50'],
|
||||
'nutritional_values.*.unit' => ['required_with:nutritional_values.*.value', new Enum(NutritionnalUnit::class)],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom attributes for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'name' => trans('validation.attributes.name'),
|
||||
'seo_title' => trans('validation.attributes.seo_title'),
|
||||
'seo_description' => trans('validation.attributes.seo_description'),
|
||||
'category' => trans('validation.attributes.category'),
|
||||
'technology' => trans('validation.attributes.technology'),
|
||||
'is_egalim' => trans('validation.attributes.is_egalim'),
|
||||
'breeding_methods' => trans('validation.attributes.breeding_methods'),
|
||||
'breeding_methods.*' => trans('validation.attributes.breeding_methods'),
|
||||
'businesses' => trans('validation.attributes.businesses'),
|
||||
'businesses.*' => trans('validation.attributes.businesses'),
|
||||
'formats' => trans('validation.attributes.formats'),
|
||||
'formats.*' => trans('validation.attributes.formats'),
|
||||
'description' => trans('validation.attributes.description'),
|
||||
'details_and_advantages' => trans('validation.attributes.details_and_advantages'),
|
||||
'handling_advices' => trans('validation.attributes.handling_advices'),
|
||||
'ingredients' => trans('validation.attributes.ingredients'),
|
||||
'conservation_before_opening' => trans('validation.attributes.conservation_before_opening'),
|
||||
'conservation_after_opening' => trans('validation.attributes.conservation_after_opening'),
|
||||
'picture' => trans('validation.attributes.picture'),
|
||||
'picture_alt' => trans('validation.attributes.picture_alt'),
|
||||
'picture_title' => trans('validation.attributes.picture_title'),
|
||||
'pdf' => trans('validation.attributes.pdf'),
|
||||
'nutritional_values' => trans('validation.attributes.nutritional_values'),
|
||||
'nutritional_values.refWeightG.value' => trans('validation.attributes.nutritional_refWeightG'),
|
||||
'nutritional_values.kj.value' => trans('validation.attributes.nutritional_kj'),
|
||||
'nutritional_values.kcal.value' => trans('validation.attributes.nutritional_kcal'),
|
||||
'nutritional_values.fatG.value' => trans('validation.attributes.nutritional_fatG'),
|
||||
'nutritional_values.satFatG.value' => trans('validation.attributes.nutritional_satFatG'),
|
||||
'nutritional_values.carbG.value' => trans('validation.attributes.nutritional_carbG'),
|
||||
'nutritional_values.sugarG.value' => trans('validation.attributes.nutritional_sugarG'),
|
||||
'nutritional_values.fiberG.value' => trans('validation.attributes.nutritional_fiberG'),
|
||||
'nutritional_values.proteinG.value' => trans('validation.attributes.nutritional_proteinG'),
|
||||
'nutritional_values.saltG.value' => trans('validation.attributes.nutritional_saltG'),
|
||||
'nutritional_values.*.unit' => trans('validation.attributes.nutritional_values_unit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
91
app/Http/Requests/Back/Products/UpdateProductRequest.php
Normal file
91
app/Http/Requests/Back/Products/UpdateProductRequest.php
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Back\Products;
|
||||
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class UpdateProductRequest extends StoreProductRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return array_merge(parent::rules(), [
|
||||
'pdf' => [
|
||||
'nullable',
|
||||
$this->validatePdf(...),
|
||||
],
|
||||
'picture' => [
|
||||
'required',
|
||||
$this->validatePicture(...),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function validatePdf(string $attribute, mixed $value, \Closure $fail): void
|
||||
{
|
||||
$valuePath = match (true) {
|
||||
is_string($value) => $value,
|
||||
$value instanceof UploadedFile => $value->getPathname(),
|
||||
default => null
|
||||
};
|
||||
|
||||
if (is_string($valuePath)) {
|
||||
$this->validateMaxFileSize($attribute, $valuePath, $fail);
|
||||
|
||||
ToolboxHelper::validatePdf(
|
||||
$attribute,
|
||||
$valuePath,
|
||||
$fail
|
||||
);
|
||||
} elseif ($value !== null) {
|
||||
$fail('Unexpected value for pdf');
|
||||
}
|
||||
}
|
||||
|
||||
private function validatePicture(string $attribute, mixed $value, \Closure $fail): void
|
||||
{
|
||||
$valuePath = match (true) {
|
||||
is_string($value) => $value,
|
||||
$value instanceof UploadedFile => $value->getPathname(),
|
||||
default => null
|
||||
};
|
||||
|
||||
if (is_string($valuePath)) {
|
||||
$this->validateMaxFileSize($attribute, $valuePath, $fail);
|
||||
|
||||
ToolboxHelper::validatePicture(
|
||||
$attribute,
|
||||
$valuePath,
|
||||
$fail,
|
||||
(object) [
|
||||
'minWidth' => self::PICTURE_WIDTH,
|
||||
'maxWidth' => self::PICTURE_WIDTH,
|
||||
'minHeight' => self::PICTURE_HEIGHT,
|
||||
'maxHeight' => self::PICTURE_HEIGHT,
|
||||
]
|
||||
);
|
||||
} elseif ($value !== null) {
|
||||
$fail('Unexpected value for picture');
|
||||
}
|
||||
}
|
||||
|
||||
private function validateMaxFileSize(string $attribute, string $valuePath, \Closure $fail): void
|
||||
{
|
||||
$sizeInBytes = match (true) {
|
||||
Storage::exists($valuePath) => Storage::size($valuePath),
|
||||
File::exists($valuePath) => File::size($valuePath),
|
||||
default => 0,
|
||||
};
|
||||
|
||||
if (($sizeInBytes / 1024) > self::MAX_FILE_SIZE_KB) {
|
||||
$fail(trans('validation.max.file', [
|
||||
'attribute' => $this->attributes()[$attribute] ?? $attribute,
|
||||
'max' => self::MAX_FILE_SIZE_KB,
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
42
app/Http/Requests/Back/Recipes/IndexRecipeRequest.php
Normal file
42
app/Http/Requests/Back/Recipes/IndexRecipeRequest.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Back\Recipes;
|
||||
|
||||
use App\Services\AdminAuthService;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class IndexRecipeRequest extends FormRequest
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AdminAuthService $authService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->authService->check();
|
||||
}
|
||||
|
||||
public function getSearchValue(): ?string
|
||||
{
|
||||
return $this->has('search') ?
|
||||
$this->string('search')->value() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Validation\Rule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'search' => ['sometimes', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
100
app/Http/Requests/Back/Recipes/StoreRecipeRequest.php
Normal file
100
app/Http/Requests/Back/Recipes/StoreRecipeRequest.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Back\Recipes;
|
||||
|
||||
use App\Services\AdminAuthService;
|
||||
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\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Rules\Enum;
|
||||
|
||||
class StoreRecipeRequest extends FormRequest
|
||||
{
|
||||
protected const int MAX_FILE_SIZE_KB = 5120;
|
||||
|
||||
protected const int PICTURE_WIDTH = 800;
|
||||
|
||||
protected const int PICTURE_HEIGHT = 800;
|
||||
|
||||
public function __construct(
|
||||
private readonly AdminAuthService $authService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->authService->check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Validation\Rule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'seo_title' => ['required', 'string', 'between:45,70'],
|
||||
'seo_description' => ['required', 'string', 'between:50,160'],
|
||||
'product_category' => ['required', new Enum(ProductCategory::class)],
|
||||
'consomption_time' => ['required', new Enum(ConsumptionTime::class)],
|
||||
'inspiration' => ['required', new Enum(Inspiration::class)],
|
||||
'season' => ['required', new Enum(Season::class)],
|
||||
'description' => ['nullable', 'string'],
|
||||
'details' => ['nullable', 'string'],
|
||||
'ingredients_list' => ['required', 'string'],
|
||||
'preparation_steps' => ['required', 'string'],
|
||||
'tips_and_advices' => ['nullable', 'string'],
|
||||
'picture' => [
|
||||
'required',
|
||||
Rule::imageFile()
|
||||
->max(self::MAX_FILE_SIZE_KB)
|
||||
->dimensions(
|
||||
Rule::dimensions()
|
||||
->width(static::PICTURE_WIDTH)
|
||||
->height(static::PICTURE_HEIGHT)
|
||||
),
|
||||
],
|
||||
'picture_alt' => ['required', 'string', 'min:4', 'max:75'],
|
||||
'picture_title' => ['required', 'string', 'min:4', 'max:75'],
|
||||
'products' => ['nullable', 'array'],
|
||||
'products.*' => ['string', 'uuid', 'exists:products,id'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom attributes for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'name' => trans('validation.attributes.name'),
|
||||
'seo_title' => trans('validation.attributes.seo_title'),
|
||||
'seo_description' => trans('validation.attributes.seo_description'),
|
||||
'product_category' => trans('validation.attributes.product_category'),
|
||||
'consomption_time' => trans('validation.attributes.consomption_time'),
|
||||
'inspiration' => trans('validation.attributes.inspiration'),
|
||||
'season' => trans('validation.attributes.season'),
|
||||
'description' => trans('validation.attributes.description'),
|
||||
'details' => trans('validation.attributes.details'),
|
||||
'ingredients_list' => trans('validation.attributes.ingredients_list'),
|
||||
'preparation_steps' => trans('validation.attributes.preparation_steps'),
|
||||
'tips_and_advices' => trans('validation.attributes.tips_and_advices'),
|
||||
'picture' => trans('validation.attributes.picture'),
|
||||
'picture_alt' => trans('validation.attributes.picture_alt'),
|
||||
'picture_title' => trans('validation.attributes.picture_title'),
|
||||
'products' => trans('validation.attributes.associated_products'),
|
||||
];
|
||||
}
|
||||
}
|
||||
71
app/Http/Requests/Back/Recipes/UpdateRecipeRequest.php
Normal file
71
app/Http/Requests/Back/Recipes/UpdateRecipeRequest.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Back\Recipes;
|
||||
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class UpdateRecipeRequest extends StoreRecipeRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Validation\Rule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return array_merge(parent::rules(), [
|
||||
'picture' => [
|
||||
'required',
|
||||
$this->validatePicture(...),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function validatePicture(string $attribute, mixed $value, \Closure $fail): void
|
||||
{
|
||||
$valuePath = match (true) {
|
||||
is_string($value) => $value,
|
||||
$value instanceof UploadedFile => $value->getPathname(),
|
||||
default => null
|
||||
};
|
||||
|
||||
if (is_string($valuePath)) {
|
||||
$this->validateMaxFileSize($attribute, $valuePath, $fail);
|
||||
|
||||
ToolboxHelper::validatePicture(
|
||||
$attribute,
|
||||
$valuePath,
|
||||
$fail,
|
||||
(object) [
|
||||
'minWidth' => self::PICTURE_WIDTH,
|
||||
'maxWidth' => self::PICTURE_WIDTH,
|
||||
'minHeight' => self::PICTURE_HEIGHT,
|
||||
'maxHeight' => self::PICTURE_HEIGHT,
|
||||
]
|
||||
);
|
||||
} elseif ($value !== null) {
|
||||
$fail('Unexpected value for picture');
|
||||
}
|
||||
}
|
||||
|
||||
private function validateMaxFileSize(string $attribute, string $valuePath, \Closure $fail): void
|
||||
{
|
||||
$sizeInBytes = match (true) {
|
||||
Storage::exists($valuePath) => Storage::size($valuePath),
|
||||
File::exists($valuePath) => File::size($valuePath),
|
||||
default => 0,
|
||||
};
|
||||
|
||||
if (($sizeInBytes / 1024) > self::MAX_FILE_SIZE_KB) {
|
||||
$fail(trans('validation.max.file', [
|
||||
'attribute' => $this->attributes()[$attribute] ?? $attribute,
|
||||
'max' => self::MAX_FILE_SIZE_KB,
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
124
app/Http/Requests/Front/Products/IndexProductRequest.php
Normal file
124
app/Http/Requests/Front/Products/IndexProductRequest.php
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Front\Products;
|
||||
|
||||
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 Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class IndexProductRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* @return \Domain\Models\ValueObjects\Products\ProductCategory[]
|
||||
*/
|
||||
public function getCategories(): array
|
||||
{
|
||||
return array_map(
|
||||
function ($category) {
|
||||
if (! is_string($category)) {
|
||||
throw new \InvalidArgumentException('Category must be a string');
|
||||
}
|
||||
|
||||
return ProductCategory::from($category);
|
||||
},
|
||||
$this->array('category'),
|
||||
);
|
||||
}
|
||||
|
||||
/** @return \Domain\Models\ValueObjects\Products\BreedingMethod[] */
|
||||
public function getBreedingMethods(): array
|
||||
{
|
||||
return array_map(
|
||||
function ($breeding) {
|
||||
if (! is_string($breeding)) {
|
||||
throw new \InvalidArgumentException('Breeding method must be a string');
|
||||
}
|
||||
|
||||
return BreedingMethod::from($breeding);
|
||||
},
|
||||
$this->array('breeding'),
|
||||
);
|
||||
}
|
||||
|
||||
/** @return \Domain\Models\ValueObjects\Products\Technology[] */
|
||||
public function getTechnologies(): array
|
||||
{
|
||||
return array_map(
|
||||
function ($technology) {
|
||||
if (! is_string($technology)) {
|
||||
throw new \InvalidArgumentException('Technology must be a string');
|
||||
}
|
||||
|
||||
return Technology::from($technology);
|
||||
},
|
||||
$this->array('technology'),
|
||||
);
|
||||
}
|
||||
|
||||
/** @return \Domain\Models\ValueObjects\Products\Business[] */
|
||||
public function getBusinesses(): array
|
||||
{
|
||||
return array_map(
|
||||
function ($business) {
|
||||
if (! is_string($business)) {
|
||||
throw new \InvalidArgumentException('Business must be a string');
|
||||
}
|
||||
|
||||
return Business::from($business);
|
||||
},
|
||||
$this->array('business'),
|
||||
);
|
||||
}
|
||||
|
||||
public function getIsEgalim(): ?bool
|
||||
{
|
||||
$egalim = $this->input('egalim');
|
||||
if (is_array($egalim) && (in_array(1, $egalim, true) || in_array('1', $egalim, true))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getSearchValue(): ?string
|
||||
{
|
||||
return $this->has('search') && ! empty($this->string('search')->value()) ?
|
||||
$this->string('search')->value() : null;
|
||||
}
|
||||
|
||||
public function hasFilter(): bool
|
||||
{
|
||||
return count($this->getCategories()) > 0
|
||||
|| count($this->getBreedingMethods()) > 0
|
||||
|| count($this->getTechnologies()) > 0
|
||||
|| count($this->getBusinesses()) > 0
|
||||
|| $this->getIsEgalim() !== null
|
||||
|| $this->getSearchValue() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Validation\Rule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'search' => ['nullable', 'string'],
|
||||
'category' => ['nullable', 'array'],
|
||||
'category.*' => ['string'],
|
||||
'breeding' => ['nullable', 'array'],
|
||||
'breeding.*' => ['string'],
|
||||
'technology' => ['nullable', 'array'],
|
||||
'technology.*' => ['string'],
|
||||
'business' => ['nullable', 'array'],
|
||||
'business.*' => ['string'],
|
||||
'egalim' => ['nullable', 'array'],
|
||||
'egalim.*' => ['numeric'],
|
||||
];
|
||||
}
|
||||
}
|
||||
100
app/Http/Requests/Front/Recipes/IndexRecipeRequest.php
Normal file
100
app/Http/Requests/Front/Recipes/IndexRecipeRequest.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Front\Recipes;
|
||||
|
||||
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\Foundation\Http\FormRequest;
|
||||
|
||||
class IndexRecipeRequest extends FormRequest
|
||||
{
|
||||
/** @return \Domain\Models\ValueObjects\Products\ProductCategory[] */
|
||||
public function getCategories(): array
|
||||
{
|
||||
return array_map(
|
||||
function ($category) {
|
||||
if (! is_string($category)) {
|
||||
throw new \InvalidArgumentException('Category must be a string');
|
||||
}
|
||||
|
||||
return ProductCategory::from($category);
|
||||
},
|
||||
$this->array('category'),
|
||||
);
|
||||
}
|
||||
|
||||
/** @return \Domain\Models\ValueObjects\Recipes\ConsumptionTime[] */
|
||||
public function getConsumptionTimes(): array
|
||||
{
|
||||
return array_map(
|
||||
function ($time) {
|
||||
if (! is_string($time)) {
|
||||
throw new \InvalidArgumentException('Consumption time must be a string');
|
||||
}
|
||||
|
||||
return ConsumptionTime::from($time);
|
||||
},
|
||||
$this->array('consumption_time'),
|
||||
);
|
||||
}
|
||||
|
||||
/** @return \Domain\Models\ValueObjects\Recipes\Inspiration[] */
|
||||
public function getInspirations(): array
|
||||
{
|
||||
return array_map(
|
||||
function ($inspiration) {
|
||||
if (! is_string($inspiration)) {
|
||||
throw new \InvalidArgumentException('Inspiration must be a string');
|
||||
}
|
||||
|
||||
return Inspiration::from($inspiration);
|
||||
},
|
||||
$this->array('inspiration'),
|
||||
);
|
||||
}
|
||||
|
||||
/** @return \Domain\Models\ValueObjects\Recipes\Season[] */
|
||||
public function getSeasons(): array
|
||||
{
|
||||
return array_map(
|
||||
function ($season) {
|
||||
if (! is_string($season)) {
|
||||
throw new \InvalidArgumentException('Season must be a string');
|
||||
}
|
||||
|
||||
return Season::from($season);
|
||||
},
|
||||
$this->array('season'),
|
||||
);
|
||||
}
|
||||
|
||||
public function getSearchValue(): ?string
|
||||
{
|
||||
return $this->has('search') && ! empty($this->string('search')->value()) ?
|
||||
$this->string('search')->value() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Validation\Rule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'search' => ['nullable', 'string'],
|
||||
'category' => ['nullable', 'array'],
|
||||
'category.*' => ['string'],
|
||||
'consumption_time' => ['nullable', 'array'],
|
||||
'consumption_time.*' => ['string'],
|
||||
'inspiration' => ['nullable', 'array'],
|
||||
'inspiration.*' => ['string'],
|
||||
'season' => ['nullable', 'array'],
|
||||
'season.*' => ['string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
41
app/Http/Resources/CommonResource.php
Normal file
41
app/Http/Resources/CommonResource.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Uwil\LaravelBackend\Resources\CommonResource as BaseCommonResource;
|
||||
|
||||
class CommonResource extends BaseCommonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$data = parent::toArray($request);
|
||||
|
||||
$resource = $this->resource;
|
||||
|
||||
if (! is_object($resource) || ! ($resource instanceof Model)) {
|
||||
throw new \InvalidArgumentException('Resource must be an instance of ' . Model::class);
|
||||
}
|
||||
|
||||
$modelClass = $resource::class;
|
||||
|
||||
$data['modelClass'] = $modelClass;
|
||||
|
||||
$data['policies'] = Arr::wrap($data['policies'] ?? []);
|
||||
|
||||
$data['policies']['create'] = Gate::allows('create', $modelClass);
|
||||
$data['policies']['viewAny'] = Gate::allows('viewAny', $modelClass);
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
529
app/Lib/Helpers/FileStorageHelper.php
Normal file
529
app/Lib/Helpers/FileStorageHelper.php
Normal file
|
|
@ -0,0 +1,529 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Lib\Helpers;
|
||||
|
||||
use App\Exceptions\Helpers\FileStorageOperationException;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* This class is used to handle files in LDC Laravel stack
|
||||
*
|
||||
* * The storage strategy will be using date.
|
||||
* The file name may contain sufix timestamp highresolution time (hrtime or microtime) if is duplicate.
|
||||
* (function() { return time() . ' ' . Str::slug(microtime(true)) . ' ' . hrtime(true); })();
|
||||
*
|
||||
* posts/ - The model table name
|
||||
* 10/ - Year
|
||||
* 04/ - Month
|
||||
* 19/ - Day
|
||||
* 5f/ - First two letters of sha256 file hash
|
||||
* black-cat-under-red-car.png - Slugified file name
|
||||
* black-cat-under-red-car-286071690833400.png - Slugified file name that is duplicated
|
||||
*
|
||||
* The final path will be storage/modelfiles/posts/10/04/19/5f/black-cat-under-red-car-286071690833400.png
|
||||
*
|
||||
* ! No limits of file per folder will be handled.
|
||||
*/
|
||||
class FileStorageHelper
|
||||
{
|
||||
private const string STORAGE_PREFIX = 'storage/';
|
||||
|
||||
/**
|
||||
* Remove all files from string attribute html that are stored in public
|
||||
*
|
||||
* @param string $publicStorageFilePath The file path from public directory.
|
||||
* @return string The local storage path.
|
||||
*/
|
||||
public static function getFileFullPath(string $publicStorageFilePath): string
|
||||
{
|
||||
/** @var \Illuminate\Filesystem\FilesystemAdapter */
|
||||
$disk = Storage::disk('public');
|
||||
$path = Str::replace(self::STORAGE_PREFIX, '', \ltrim(\urldecode($publicStorageFilePath), '/'));
|
||||
|
||||
return $disk->path($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the file associate with the attribute.
|
||||
*/
|
||||
public static function removeFile(Model $model, string $attribute): void
|
||||
{
|
||||
$attributeValue = $model->getAttribute($attribute);
|
||||
if (! is_scalar($attributeValue)) {
|
||||
return;
|
||||
}
|
||||
$path = \ltrim(\urldecode(strval($attributeValue)), '/');
|
||||
if (! str_starts_with($path, 'storage')) {
|
||||
return;
|
||||
}
|
||||
$path = Str::replace(self::STORAGE_PREFIX, '', $path);
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the file associate with the attribute old value (before updated to DB).
|
||||
*/
|
||||
public static function removeOldFile(Model $model, string $attribute, bool $onlyIfUpdated = true): void
|
||||
{
|
||||
$originalValue = ToolboxHelper::getOriginal($model, $attribute);
|
||||
// * Ignore If removal has to be done only if the field has changed and it actually hasnt.
|
||||
if ($onlyIfUpdated && $model->{$attribute} === $originalValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! is_string($originalValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$originalPath = \ltrim(\urldecode($originalValue), '/');
|
||||
if (! str_starts_with($originalPath, 'storage')) {
|
||||
return;
|
||||
}
|
||||
$originalPath = Str::replace(self::STORAGE_PREFIX, '', $originalPath);
|
||||
if (Storage::disk('public')->exists($originalPath)) {
|
||||
Storage::disk('public')->delete($originalPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a file using optimized storage strategy
|
||||
*
|
||||
* You can use filename param to overload file name.
|
||||
*
|
||||
* @param Model $model The model that will hold the picture.
|
||||
* @param UploadedFile|\SplFileInfo|string $file The file, will just return itself is string.
|
||||
* @param bool $slugify Force filename slugification.
|
||||
* @param string|null $filename This string may be used to store the file.
|
||||
* @param bool $private To store the file in a private space.
|
||||
*
|
||||
* @throws FileStorageOperationException If file copy fails.
|
||||
*
|
||||
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
|
||||
*/
|
||||
public static function storeFile(
|
||||
Model $model,
|
||||
UploadedFile | \SplFileInfo | string | null $file,
|
||||
bool $slugify = false,
|
||||
?string $filename = null,
|
||||
bool $private = false
|
||||
): string {
|
||||
// phpcs:enable
|
||||
$tableName = $model->getTable();
|
||||
if ($file instanceof UploadedFile) {
|
||||
return self::storeUploadedFile($file, $tableName, $slugify, $filename, $private);
|
||||
}
|
||||
|
||||
if ($file instanceof \SplFileInfo) {
|
||||
return self::storeSplFileInfo($file, $tableName, $slugify, $filename, $private);
|
||||
}
|
||||
|
||||
return (string) $file;
|
||||
}
|
||||
|
||||
private static function storeUploadedFile(
|
||||
UploadedFile $file,
|
||||
string $tableName,
|
||||
bool $slugify,
|
||||
?string $filename,
|
||||
bool $private
|
||||
): string {
|
||||
$filename = $filename ?: $file->getClientOriginalName();
|
||||
$filename = $slugify ? self::slugifyFileName($filename) : $filename;
|
||||
$disk = $private ? 'private' : 'public';
|
||||
$newPath = str_replace(
|
||||
storage_path("app/{$disk}"),
|
||||
'',
|
||||
self::prepareMoveFile($tableName, $filename, $private)
|
||||
);
|
||||
|
||||
$directory = pathinfo($newPath, \PATHINFO_DIRNAME);
|
||||
$basename = pathinfo($newPath, \PATHINFO_BASENAME);
|
||||
|
||||
if (! $private) {
|
||||
$file->storePubliclyAs(
|
||||
$directory,
|
||||
$basename,
|
||||
['disk' => $disk]
|
||||
);
|
||||
} else {
|
||||
$file->storeAs(
|
||||
$directory,
|
||||
$basename,
|
||||
['disk' => $disk]
|
||||
);
|
||||
}
|
||||
|
||||
return "storage{$newPath}";
|
||||
}
|
||||
|
||||
private static function storeSplFileInfo(
|
||||
\SplFileInfo $file,
|
||||
string $tableName,
|
||||
bool $slugify,
|
||||
?string $filename,
|
||||
bool $private
|
||||
): string {
|
||||
$filename = $filename ?: $file->getBaseName();
|
||||
$filename = $slugify ? self::slugifyFileName($filename) : $filename;
|
||||
$newPath = self::prepareMoveFile($tableName, $filename, $private);
|
||||
|
||||
if (! File::copy($file->getRealPath(), $newPath)) {
|
||||
throw FileStorageOperationException::copyFailed($file->getRealPath(), $newPath);
|
||||
}
|
||||
|
||||
if ($private) {
|
||||
return \ltrim(str_replace(\storage_path('app/private'), '/storage/app/private', $newPath), '/');
|
||||
}
|
||||
|
||||
return \ltrim(str_replace(\storage_path('app/public'), '/storage', $newPath), '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store all files from string attribute html
|
||||
* If oldHtmlString is passed, a diff is done with new and old files
|
||||
* to keep only files that are present in the new html string.
|
||||
*
|
||||
* @param Model $model The model that will hold the picture.
|
||||
* @param string|null $htmlString The html string to parse and modify files.
|
||||
* @param string|null $oldHtmlString The old html string with alreadyParsed
|
||||
* files using storeModelFilesFromHtml.
|
||||
* @return string|null Html with files pathes replaced using storage strategy.
|
||||
*/
|
||||
public static function storeModelFilesFromHtml(
|
||||
Model $model,
|
||||
?string $htmlString,
|
||||
?string $oldHtmlString = null
|
||||
): ?string {
|
||||
if (is_null($htmlString)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Collection::make(self::onlyUploadedFilesWithFileManager(self::findFilesInHtml($htmlString)))
|
||||
->each(function (string $path) use ($model, &$htmlString): void {
|
||||
/** @var \Illuminate\Filesystem\FilesystemAdapter */
|
||||
$disk = Storage::disk('private');
|
||||
$fileInfo = new \SplFileInfo($disk->path(
|
||||
ToolboxHelper::mbReplaceSingle('/uploads/', '', \urldecode($path))
|
||||
));
|
||||
$htmlString = ToolboxHelper::mbReplaceSingle(
|
||||
$path,
|
||||
'/' . self::storeFile($model, $fileInfo, true),
|
||||
$htmlString
|
||||
);
|
||||
});
|
||||
|
||||
if ($oldHtmlString) {
|
||||
self::removeOldModelFilesFromHtml($htmlString, $oldHtmlString);
|
||||
}
|
||||
|
||||
return $htmlString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all old files from string attribute html that are stored in public that
|
||||
* are nove present anymore on new html string
|
||||
*
|
||||
* @param string $newHtmlString The html string with alreadyParsed files using storeModelFilesFromHtml.
|
||||
* @param string $oldHtmlString The old html string with alreadyParsed files using storeModelFilesFromHtml.
|
||||
*/
|
||||
public static function removeOldModelFilesFromHtml(string $newHtmlString, string $oldHtmlString): void
|
||||
{
|
||||
self::removeOldFiles(self::findFilesInHtml($oldHtmlString), self::findFilesInHtml($newHtmlString));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all old files that are stored in public that
|
||||
* are not present anymore on new array files
|
||||
*
|
||||
* @param string[] $oldFiles The old array files.
|
||||
* @param string[] $newFiles The new array files.
|
||||
*/
|
||||
public static function removeOldFiles(array $oldFiles, array $newFiles): void
|
||||
{
|
||||
$oldFiles = Collection::make(self::onlyUploadedFilesWithFileStorageHelper($oldFiles));
|
||||
$filesToKeep = Collection::make(self::onlyUploadedFilesWithFileStorageHelper($newFiles));
|
||||
$filesToRemove = $oldFiles->diff($filesToKeep);
|
||||
$filesToRemove->each(function (string $publicStorageFilePath): void {
|
||||
$path = Str::replace(self::STORAGE_PREFIX, '', \ltrim(\urldecode($publicStorageFilePath), '/'));
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all files from string attribute html that are stored in public
|
||||
*
|
||||
* @param string $htmlString The html string with alreadyParsed files using storeModelFilesFromHtml.
|
||||
*/
|
||||
public static function removeModelFilesOfHtml(string $htmlString): void
|
||||
{
|
||||
$filesToRemove = Collection::make(self::onlyUploadedFilesWithFileStorageHelper(self::findFilesInHtml($htmlString)));
|
||||
$filesToRemove->each(function (string $publicStorageFilePath): void {
|
||||
$path = Str::replace(self::STORAGE_PREFIX, '', \ltrim(\urldecode($publicStorageFilePath), '/'));
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all occurence of files in html
|
||||
*
|
||||
* @param string $html The html string to parse and find files.
|
||||
* @return string[] The array of files found in html.
|
||||
*/
|
||||
public static function findFilesInHtml(string $html): array
|
||||
{
|
||||
preg_match_all(
|
||||
'/(?:href|src|url)="([^"]*)"/Ui',
|
||||
$html,
|
||||
$matches
|
||||
);
|
||||
|
||||
return Collection::make($matches[1])->unique()->filter()->values()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter array to get only uploaded files that exists
|
||||
* on disk 'public' (using helper storage strategy)
|
||||
*
|
||||
* @param string[] $urls The array of urls to filter
|
||||
* @return string[] The filtered array with only uploaded files that exists on disk 'public
|
||||
*/
|
||||
public static function onlyUploadedFilesWithFileStorageHelper(array $urls): array
|
||||
{
|
||||
return Collection::make($urls)->unique()
|
||||
// * Filter to get only /uploads and using app url or empty
|
||||
->filter(function (string $url) {
|
||||
$path = (string) (\parse_url($url, \PHP_URL_PATH) ?? '');
|
||||
|
||||
return str_starts_with(\ltrim($path, '/'), 'storage/modelfiles');
|
||||
})->filter(function (string $url) {
|
||||
$path = (string) (\parse_url($url, \PHP_URL_PATH) ?? '');
|
||||
|
||||
return Storage::disk('public')
|
||||
->exists(Str::replace(self::STORAGE_PREFIX, '', \ltrim(\urldecode($path), '/')));
|
||||
})->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter array to get only uploaded files that exist on disk 'private'
|
||||
*
|
||||
* @param string[] $urls The array of urls to filter
|
||||
* @return string[] The filtered array
|
||||
*/
|
||||
private static function onlyUploadedFilesWithFileManager(array $urls): array
|
||||
{
|
||||
$appUrl = Config::string('app.url');
|
||||
|
||||
if (empty($appUrl)) {
|
||||
throw new \UnexpectedValueException('App URL must be set in config to filter uploaded files with file manager');
|
||||
}
|
||||
$appHost = parse_url((string) $appUrl, PHP_URL_HOST);
|
||||
|
||||
return Collection::make($urls)->unique()->filter(function (string $url) use ($appHost) {
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
|
||||
// 1. Validation du domaine (Doit être identique à l'app ou être un chemin relatif)
|
||||
if ($host !== null && $host !== $appHost) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Extraction et nettoyage du chemin
|
||||
$path = ltrim(urldecode((string) parse_url($url, PHP_URL_PATH)), '/');
|
||||
|
||||
// 3. Vérification du préfixe 'uploads/' et existence sur le disque
|
||||
if (! str_starts_with($path, 'uploads/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// On retire 'uploads/' pour tester l'existence sur le disque 'private'
|
||||
$diskPath = substr($path, strlen('uploads/'));
|
||||
|
||||
return Storage::disk('private')->exists($diskPath);
|
||||
})->values()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare storage path and return
|
||||
* the file full path
|
||||
*
|
||||
* @param string $tableName Model table name.
|
||||
* @param string $filename The basename for the file.
|
||||
* @param bool $private May the file be stored in a private dir.
|
||||
*/
|
||||
private static function prepareMoveFile(string $tableName, string $filename, bool $private = false): string
|
||||
{
|
||||
// * Security filter
|
||||
$filename = \htmlspecialchars($filename);
|
||||
// * Security filter + beautifier
|
||||
$filename = self::sanitizeFileName($filename);
|
||||
// * Slugify filename
|
||||
$ext = \pathinfo($filename, \PATHINFO_EXTENSION);
|
||||
$filename = Str::slug(\pathinfo($filename, \PATHINFO_FILENAME)) .
|
||||
(\strlen($ext) ? ".{$ext}" : '');
|
||||
|
||||
$folderPath = self::getStoragePath(
|
||||
$private ? 'app/private/modelfiles' : 'app/public/modelfiles',
|
||||
$tableName,
|
||||
$filename
|
||||
);
|
||||
$filename = self::getFileUniqueName($folderPath, $filename);
|
||||
|
||||
return "{$folderPath}/{$filename}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sub folders to store files using pattern
|
||||
* storage/modelfiles/posts/10/04/19/5f
|
||||
* year/month/day/sha256 first 2 letters
|
||||
*/
|
||||
private static function getStoragePath(string $storage_dir, string $table, string $filename): string
|
||||
{
|
||||
$filenameHash = \hash('sha256', $filename);
|
||||
$pathFolder = \storage_path(sprintf(
|
||||
'%s/%s/%s/%s/%s/%s',
|
||||
$storage_dir,
|
||||
$table,
|
||||
gmdate('y'),
|
||||
gmdate('m'),
|
||||
gmdate('d'),
|
||||
substr($filenameHash, 0, 2)
|
||||
));
|
||||
if (! File::exists($pathFolder)) {
|
||||
File::makeDirectory($pathFolder, 0755, true);
|
||||
}
|
||||
|
||||
return $pathFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique filename if the path is already occupied
|
||||
*
|
||||
* @throws FileStorageOperationException If a unique filename cannot be obtained.
|
||||
*/
|
||||
private static function getFileUniqueName(string $folderPath, string $filename): string
|
||||
{
|
||||
$attempts = 0;
|
||||
$maxAttempts = 20;
|
||||
while (File::exists("{$folderPath}/{$filename}")) {
|
||||
if ($attempts++ >= $maxAttempts) {
|
||||
throw FileStorageOperationException::cannotGenerateUniqueFilename($folderPath, $filename);
|
||||
}
|
||||
$filename = Str::of(pathinfo($filename, \PATHINFO_FILENAME))
|
||||
->append(Str::random(8) . '-')
|
||||
->append('.')
|
||||
->append(pathinfo($filename, \PATHINFO_EXTENSION))
|
||||
->toString();
|
||||
}
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slugify the filename
|
||||
*/
|
||||
public static function slugifyFileName(string $filename): string
|
||||
{
|
||||
return Str::slug(pathinfo($filename, \PATHINFO_FILENAME)) . '.' . pathinfo($filename, \PATHINFO_EXTENSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize File name for uploaded files
|
||||
*
|
||||
* @param string $filename The original uploaded client file name.
|
||||
* @param bool $beautify Beautify the filename.
|
||||
* @return string The sanitized file name.
|
||||
*/
|
||||
public static function sanitizeFileName(string $filename, bool $beautify = true): string
|
||||
{
|
||||
$filename = preg_replace('/[<>:"\/\\|?*\x00-\x1F\x7F\xA0\xAD#\[\]@!$&\'()+,;={}^~`]/u', '-', $filename) ?? $filename;
|
||||
// * Avoids '.' '..' or '.hiddenFiles' .
|
||||
$filename = (string) Str::of($filename)->ltrim('.-');
|
||||
// * Optional beautification
|
||||
if ($beautify) {
|
||||
$filename = self::beautifyFilename($filename);
|
||||
}
|
||||
// * Maximize filename length to 255 bytes http://serverfault.com/a/9548/44086
|
||||
/** @var string */
|
||||
$extension = pathinfo($filename, \PATHINFO_EXTENSION);
|
||||
/** @var string */
|
||||
$filename = pathinfo($filename, \PATHINFO_FILENAME);
|
||||
|
||||
$encoding = mb_detect_encoding($filename);
|
||||
$encoding = $encoding ?: null;
|
||||
|
||||
return \mb_strcut(
|
||||
$filename,
|
||||
0,
|
||||
255 - (! empty($extension) ? strlen($extension) + 1 : 0),
|
||||
$encoding
|
||||
) . (! empty($extension) ? ".$extension" : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Beautify a filename
|
||||
*/
|
||||
private static function beautifyFilename(string $filename): string
|
||||
{
|
||||
// Reduce consecutive characters.
|
||||
$filename = preg_replace([
|
||||
// * "file name.zip" becomes 'file-name.zip'
|
||||
'/ +/',
|
||||
// * "file___name.zip" becomes 'file-name.zip'
|
||||
'/_+/',
|
||||
// * "file---name.zip" becomes 'file-name.zip'
|
||||
'/-+/',
|
||||
], '-', $filename) ?? $filename;
|
||||
$filename = preg_replace([
|
||||
// * "file--.--.-.--name.zip" becomes "file.name.zip"
|
||||
'/-*\.-*/',
|
||||
// * "file...name..zip" becomes "file.name.zip"
|
||||
'/\.{2,}/',
|
||||
], '.', $filename) ?? $filename;
|
||||
// Lowercase for windows/unix interoperability http://support.microsoft.com/kb/100625 .
|
||||
$encoding = mb_detect_encoding($filename);
|
||||
$filename = mb_strtolower($filename, $encoding ?: 'UTF-8');
|
||||
// * ".file-name.-" becomes "file-name"
|
||||
$filename = trim($filename, '.-');
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all model files for the given Model.
|
||||
*/
|
||||
public static function removeModelFiles(Model $model): void
|
||||
{
|
||||
$tableName = $model::query()->getModel()->getTable();
|
||||
|
||||
if (! empty(Storage::disk('public')->allFiles("modelfiles/{$tableName}"))) {
|
||||
Storage::disk('public')->deleteDirectory("modelfiles/{$tableName}");
|
||||
}
|
||||
if (! empty(Storage::disk('private')->allFiles("modelfiles/{$tableName}"))) {
|
||||
Storage::disk('private')->deleteDirectory("modelfiles/{$tableName}");
|
||||
}
|
||||
}
|
||||
|
||||
public static function isMimeType(string $mimeType, \SplFileInfo $file): bool
|
||||
{
|
||||
if (! $file->getRealPath()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$actualMime = File::mimeType($file->getRealPath());
|
||||
|
||||
return $actualMime ? Str::is($mimeType, $actualMime) : false;
|
||||
}
|
||||
}
|
||||
408
app/Lib/Helpers/ToolboxHelper.php
Normal file
408
app/Lib/Helpers/ToolboxHelper.php
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Lib\Helpers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ToolboxHelper extends ToolboxHelperCore
|
||||
{
|
||||
/**
|
||||
* @param array<int|string, mixed> $array1
|
||||
* @param array<int|string, mixed> $array2
|
||||
* @return array<int|string, mixed>
|
||||
*/
|
||||
public static function arrayMergeRecursiveDistinct(array &$array1, array &$array2): array
|
||||
{
|
||||
$merged = $array1;
|
||||
foreach ($array2 as $key => &$value) {
|
||||
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
|
||||
$merged[$key] = self::arrayMergeRecursiveDistinct($merged[$key], $value);
|
||||
} else {
|
||||
$merged[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip scheme (https) on oembed urls
|
||||
* Only embed allowed are for MP4 self hosted videos right now
|
||||
*
|
||||
* https is force by ckeditor MediaElements which natively supports
|
||||
* only cloud hosted medias (youtube, spotify etc..)
|
||||
*/
|
||||
public static function stripCkEditorSchemesOnMediaInputs(string $input): string
|
||||
{
|
||||
return Str::of($input)
|
||||
->replaceMatches('/(?<=<oembed url=")(http:\/\/)(?=.*?">)/m', '')
|
||||
->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the field is unique.
|
||||
*
|
||||
* @param array<mixed>|string|int|float|bool|null $value
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException If the field is not unique.
|
||||
*/
|
||||
public static function assertFieldIsUnique(
|
||||
string $table,
|
||||
string $field,
|
||||
string | int | float | bool | null | array $value,
|
||||
null | int | string $id = null,
|
||||
\Closure | string | null $query = null
|
||||
): void {
|
||||
if (! $query) {
|
||||
Validator::make([$field => $value], [
|
||||
(string) str_replace('.', '\.', $field) => Rule::unique($table, $field)->ignore($id),
|
||||
], [
|
||||
(string) str_replace('.', '\.', "{$field}.unique") => trans(
|
||||
'crud.libs.toolbox.assertFieldIsUnique',
|
||||
['attribute' => $field, 'value' => is_array($value) ? json_encode($value) : $value]
|
||||
),
|
||||
])->validate();
|
||||
|
||||
return;
|
||||
}
|
||||
Validator::make([$field => $value], [
|
||||
$field => Rule::unique($table)->where($query)->ignore($id),
|
||||
], [
|
||||
$field . '.unique' => trans(
|
||||
'crud.libs.toolbox.assertFieldIsUnique',
|
||||
['attribute' => $field, 'value' => is_array($value) ? json_encode($value) : $value]
|
||||
),
|
||||
])->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert That no Back Office url is present on txt
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
|
||||
*/
|
||||
public static function assertNoInternalBoUrls(string $field, string $text): string
|
||||
{
|
||||
$routes = Route::getRoutes();
|
||||
|
||||
return self::replaceUrlsInText($text, function (string $url) use ($routes, $field): void {
|
||||
$request = Request::create($url);
|
||||
if (! self::isInternalHost($url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (trim($routes->match($request)->getPrefix() ?? '', '/') === 'bo') {
|
||||
throw ValidationException::withMessages([
|
||||
$field => trans(
|
||||
'crud.libs.toolbox.assertNoInternalBoUrls',
|
||||
['attribute' => $field]
|
||||
),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert That only external url is present on txt
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
|
||||
*/
|
||||
public static function assertNoInternalUrls(string $field, string $text): string
|
||||
{
|
||||
self::assertNoInternalBoUrls($field, $text);
|
||||
$routes = Route::getRoutes();
|
||||
|
||||
return self::replaceUrlsInText($text, function (string $url) use ($routes, $field): void {
|
||||
$request = Request::create($url);
|
||||
$routes->match($request);
|
||||
|
||||
if (! self::isInternalHost($url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
$field => trans(
|
||||
'crud.libs.toolbox.assertNoInternalUrls',
|
||||
['attribute' => $field, 'appname' => Config::string('app.name')]
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
private static function isInternalHost(string $url): bool
|
||||
{
|
||||
return \parse_url((string) Config::string('app.url'), \PHP_URL_HOST) === \parse_url($url, \PHP_URL_HOST);
|
||||
}
|
||||
|
||||
private static function replaceUrlsInText(string $text, callable $onMatchedUrl): string
|
||||
{
|
||||
$result = preg_replace_callback(
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong
|
||||
'/(?P<url>(?:https?:\/\/)?(?:www\.)?[-a-zA-Z0-9@%._+~#=]{2,256}\.[a-z]{2,6}\b[-a-zA-Z0-9@:%_.+~#?&\/=]*)/',
|
||||
function (array $matches) use ($onMatchedUrl): string {
|
||||
$match = collect($matches)->first() ?? '';
|
||||
$url = (string) $match;
|
||||
|
||||
try {
|
||||
$onMatchedUrl($url);
|
||||
} catch (NotFoundHttpException) {
|
||||
return (string) $match;
|
||||
}
|
||||
|
||||
return (string) $match;
|
||||
},
|
||||
$text
|
||||
);
|
||||
|
||||
return is_string($result) ? $result : $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get original polyfill with autotranslate behavior
|
||||
*/
|
||||
public static function getOriginal(Model $model, string $attribute): mixed
|
||||
{
|
||||
if (
|
||||
\in_array('Spatie\Translatable\HasTranslations', \class_uses_recursive($model)) && // @phpstan-ignore-next-line
|
||||
$model->isTranslatableAttribute($attribute)
|
||||
) {
|
||||
// @phpstan-ignore-next-line
|
||||
return $model->getTranslation(
|
||||
$attribute,
|
||||
// @phpstan-ignore-next-line
|
||||
$model->getLocale(),
|
||||
// @phpstan-ignore-next-line
|
||||
$model->useFallbackLocale()
|
||||
);
|
||||
}
|
||||
|
||||
return $model->getOriginal($attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array from a dot separated string
|
||||
*
|
||||
* @param string $path The dot separated string to convert to an array.
|
||||
* @param mixed $value The value to set at the end of the array.
|
||||
* @param string $separator The separator used in the path (default is '.').
|
||||
* @return array<string,mixed> The generated array.
|
||||
*/
|
||||
public static function dotStringToNestedArray(string $path, mixed $value, string $separator = '.'): array
|
||||
{
|
||||
throw_if($separator === '', \InvalidArgumentException::class, 'Separator cannot be an empty string.');
|
||||
|
||||
$arr = $value;
|
||||
$keys = explode($separator, $path);
|
||||
$keys = array_reverse($keys);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$arr = [$key => $arr];
|
||||
}
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate pdf
|
||||
*
|
||||
* @param callable(string):void $fail A callback that should be called if the validation fails.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException If the pdf is invalid.
|
||||
*/
|
||||
public static function validatePdf(string $attribute, ?string $value, callable $fail): void
|
||||
{
|
||||
if ($value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! ($useStorage = Storage::exists($value)) && ! File::exists($value)) {
|
||||
$fail(trans('validation.custom.has_picture.file_does_not_exist', [
|
||||
'attribute' => $attribute,
|
||||
]));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (self::getUploadedFileMimeType($value, $useStorage) !== 'application/pdf') {
|
||||
$fail(trans('validation.mimetypes', [
|
||||
'attribute' => $attribute,
|
||||
'values' => 'application/pdf',
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate picture
|
||||
*
|
||||
* @param callable(string):void $fail A callback that should be called if the validation fails.
|
||||
* @param object{
|
||||
* minWidth: int,
|
||||
* maxWidth: int,
|
||||
* minHeight: int,
|
||||
* maxHeight: int
|
||||
* }&\stdClass $sizes An object containing the minimum and maximum width and height for the picture.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException If the picture is invalid.
|
||||
*/
|
||||
public static function validatePicture(string $attribute, ?string $value, callable $fail, object $sizes): void
|
||||
{
|
||||
if ($value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! ($useStorage = Storage::exists($value)) && ! File::exists($value)) {
|
||||
$fail(trans('validation.custom.has_picture.file_does_not_exist', [
|
||||
'attribute' => $attribute,
|
||||
]));
|
||||
|
||||
return;
|
||||
}
|
||||
$fileFullPath = self::getUploadedFileFullPath($value, $useStorage);
|
||||
if (
|
||||
! \in_array(self::getUploadedFileMimeType($value, $useStorage), [
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
])
|
||||
) {
|
||||
$fail(trans('validation.mimetypes', [
|
||||
'attribute' => $attribute,
|
||||
'values' => 'image/jpeg, image/jpg, image/png, image/gif',
|
||||
]));
|
||||
}
|
||||
if (! ($dimensions = \getimagesize($fileFullPath))) {
|
||||
$fail(trans('validation.dimensions', [
|
||||
'attribute' => $attribute,
|
||||
]));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$width = intval($dimensions[0]);
|
||||
$height = intval($dimensions[1]);
|
||||
|
||||
$xMessage = trans('validation.custom.has_picture.width_between', [
|
||||
'attribute' => $attribute,
|
||||
'min' => "{$sizes->minWidth}px",
|
||||
'max' => "{$sizes->maxWidth}px",
|
||||
]);
|
||||
if ($sizes->minWidth === $sizes->maxWidth) {
|
||||
$xMessage = trans('validation.custom.has_picture.width_equal', [
|
||||
'attribute' => $attribute,
|
||||
'value' => "{$sizes->minWidth}px",
|
||||
]);
|
||||
}
|
||||
$yMessage = trans('validation.custom.has_picture.height_between', [
|
||||
'attribute' => $attribute,
|
||||
'min' => "{$sizes->minHeight}px",
|
||||
'max' => "{$sizes->maxHeight}px",
|
||||
]);
|
||||
if ($sizes->minHeight === $sizes->maxHeight) {
|
||||
$yMessage = trans('validation.custom.has_picture.height_equal', [
|
||||
'attribute' => $attribute,
|
||||
'value' => "{$sizes->minHeight}px",
|
||||
]);
|
||||
}
|
||||
|
||||
if (($width < $sizes->minWidth) || ($width > $sizes->maxWidth)) {
|
||||
$fail($xMessage);
|
||||
}
|
||||
if (($height < $sizes->minHeight) || ($height > $sizes->maxHeight)) {
|
||||
$fail($yMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch class list from a specific namespace
|
||||
*
|
||||
* @return array<string> An array of classe namespaces
|
||||
*/
|
||||
public static function getClassesUsingNamespace(string $namespace): array
|
||||
{
|
||||
return collect(File::files(\app_path((string) Str::of($namespace)->replaceFirst('\App', ''))))
|
||||
->filter(fn (SplFileInfo $splFileInfo) => $splFileInfo->getExtension() === 'php')
|
||||
->map(fn (SplFileInfo $splFileInfo) => (string) Str::of($splFileInfo->getBasename())
|
||||
->replace('.' . $splFileInfo->getExtension(), '')->prepend("{$namespace}\\"))->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all classes from a specific namespace that implement a specific interface
|
||||
*
|
||||
* @return array<string> An array of classe namespaces
|
||||
*/
|
||||
public static function getClassesUsingInterface(string $namespace, string $interface): array
|
||||
{
|
||||
return Arr::where(
|
||||
static::getClassesUsingNamespace($namespace),
|
||||
fn (string $class_namespace) => \collect(class_implements($class_namespace, true) ?: [])->filter()
|
||||
->contains(fn (string $class_interface) => $class_interface === $interface)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Hex color to RGB
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public static function hexToRgb(string $hexcolor): array
|
||||
{
|
||||
$hexcolor = (string) str($hexcolor)->replaceStart('#', '');
|
||||
$r = (int) hexdec(substr($hexcolor, 0, 2));
|
||||
$g = (int) hexdec(substr($hexcolor, 2, 2));
|
||||
$b = (int) hexdec(substr($hexcolor, 4, 2));
|
||||
|
||||
return [$r, $g, $b];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text color contrast for a hex background color
|
||||
*
|
||||
* @return string "black" or "white"
|
||||
*/
|
||||
public static function getContrastYiq(string $hexcolor): string
|
||||
{
|
||||
[$r, $g, $b] = self::hexToRgb($hexcolor);
|
||||
$yiq = (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
|
||||
|
||||
return ($yiq >= 128) ? 'black' : 'white';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an HTML string to plain text with proper word spacing.
|
||||
*
|
||||
* Replaces HTML tags with spaces, strips remaining tags,
|
||||
* normalises whitespace and trims the result.
|
||||
*
|
||||
* @param string|null $html The HTML content to convert.
|
||||
* @param int|null $limit Optional character limit (uses Str::limit).
|
||||
*/
|
||||
public static function htmlToText(?string $html, ?int $limit = null): string
|
||||
{
|
||||
if ($html === null || $html === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$text = strip_tags((string) preg_replace('/<[^>]*>/', ' ', $html));
|
||||
$text = trim((string) preg_replace('/\s+/', ' ', $text));
|
||||
|
||||
if ($limit !== null) {
|
||||
return Str::limit($text, $limit);
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
191
app/Lib/Helpers/ToolboxHelperCore.php
Normal file
191
app/Lib/Helpers/ToolboxHelperCore.php
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Lib\Helpers;
|
||||
|
||||
use App\Exceptions\Helpers\UnsupportedStructureConversionException;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
abstract class ToolboxHelperCore
|
||||
{
|
||||
/**
|
||||
* Multibyte-safe str_replace with unified dispatch.
|
||||
*
|
||||
* Returns false when $replace is an array but $search is a scalar string.
|
||||
*
|
||||
* @param array<string>|string $search
|
||||
* @param array<string>|string $replace
|
||||
* @param array<int|string, mixed>|string $subject
|
||||
* @return array<int|string, mixed>|string|false
|
||||
*/
|
||||
public static function mbReplace(
|
||||
array | string $search,
|
||||
array | string $replace,
|
||||
array | string $subject,
|
||||
int &$count = 0,
|
||||
): array | string | false {
|
||||
if (is_array($subject)) {
|
||||
return self::mbReplaceInArraySubject($search, $replace, $subject, $count);
|
||||
}
|
||||
|
||||
return self::mbReplaceScalar($search, $replace, $subject, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch for scalar subject.
|
||||
* Returns false when $replace is array but $search is a scalar string.
|
||||
*
|
||||
* @param array<string>|string $search
|
||||
* @param array<string>|string $replace
|
||||
*/
|
||||
private static function mbReplaceScalar(
|
||||
array | string $search,
|
||||
array | string $replace,
|
||||
string $subject,
|
||||
int &$count = 0,
|
||||
): string | false {
|
||||
if (! is_array($search) && is_array($replace)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_array($search)) {
|
||||
return self::mbReplaceWithSearchArray($search, $replace, $subject, $count);
|
||||
}
|
||||
|
||||
if (! is_string($replace)) {
|
||||
throw new \InvalidArgumentException('Replace must be a string when search is a string.');
|
||||
}
|
||||
|
||||
return self::mbReplaceSingle($search, $replace, $subject, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[]|string $search
|
||||
* @param string[]|string $replace
|
||||
* @param array<int|string, mixed> $subject
|
||||
* @return array<int|string, mixed>
|
||||
*/
|
||||
public static function mbReplaceInArraySubject(array | string $search, array | string $replace, array $subject, int &$count = 0): array
|
||||
{
|
||||
foreach ($subject as &$value) {
|
||||
if (is_array($value)) {
|
||||
$value = self::mbReplaceInArraySubject($search, $replace, $value, $count);
|
||||
} elseif (is_array($search) && is_string($value)) {
|
||||
$value = self::mbReplaceWithSearchArray($search, $replace, $value, $count);
|
||||
} elseif (! is_array($replace) && is_string($value)) {
|
||||
$value = self::mbReplaceSingle($search, $replace, $value, $count);
|
||||
}
|
||||
}
|
||||
unset($value);
|
||||
|
||||
return $subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $search
|
||||
* @param array<string>|string $replace
|
||||
*/
|
||||
public static function mbReplaceWithSearchArray(array $search, array | string $replace, string $subject, int &$count = 0): string
|
||||
{
|
||||
if (! is_array($replace)) {
|
||||
foreach ($search as $searchItem) {
|
||||
$subject = (string) self::mbReplaceSingle($searchItem, $replace, $subject, $count);
|
||||
}
|
||||
|
||||
return $subject;
|
||||
}
|
||||
|
||||
$iterations = max(count($search), count($replace));
|
||||
$searchItem = reset($search);
|
||||
$replaceItem = reset($replace);
|
||||
|
||||
while ($iterations--) {
|
||||
$subject = (string) self::mbReplaceSingle((string) $searchItem, (string) $replaceItem, $subject, $count);
|
||||
$searchItem = next($search);
|
||||
$replaceItem = next($replace);
|
||||
}
|
||||
|
||||
return $subject;
|
||||
}
|
||||
|
||||
public static function mbReplaceSingle(string $search, string $replace, string $subject, int &$count = 0): string
|
||||
{
|
||||
$parts = mb_split(preg_quote($search), $subject) ?: [];
|
||||
$count = count($parts) - 1;
|
||||
|
||||
return implode($replace, $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert deeply an object to array
|
||||
*
|
||||
* @return array<int|string,mixed>
|
||||
*
|
||||
* @throws \App\Exceptions\Helpers\UnsupportedStructureConversionException If $input var is a scalar value.
|
||||
*/
|
||||
public static function objectToArray(mixed $input): array
|
||||
{
|
||||
if (! (is_object($input) || is_array($input))) {
|
||||
throw UnsupportedStructureConversionException::fromType($input);
|
||||
}
|
||||
|
||||
$ret = (array) $input;
|
||||
foreach ($ret as &$item) {
|
||||
$item = (is_object($item) || is_array($item)) ? self::objectToArray($item) : $item;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert deeply an array to object
|
||||
*
|
||||
* @throws \App\Exceptions\Helpers\UnsupportedStructureConversionException If $input var is a scalar value.
|
||||
*/
|
||||
public static function arrayToObject(mixed $input): object
|
||||
{
|
||||
if (! (is_object($input) || is_array($input))) {
|
||||
throw UnsupportedStructureConversionException::fromType($input);
|
||||
}
|
||||
|
||||
$ret = (object) $input;
|
||||
$retArray = (array) $ret;
|
||||
foreach ($retArray as &$item) {
|
||||
$item = (is_object($item) || is_array($item)) ? self::arrayToObject($item) : $item;
|
||||
}
|
||||
unset($item);
|
||||
|
||||
return (object) $retArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file full path
|
||||
*/
|
||||
protected static function getUploadedFileFullPath(string $value, bool $useStorage): string
|
||||
{
|
||||
/** @var \Illuminate\Filesystem\FilesystemAdapter */
|
||||
$disk = Storage::disk();
|
||||
|
||||
if ($useStorage) {
|
||||
return $disk->path($value);
|
||||
}
|
||||
|
||||
return realpath($value) ?: $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file mimetype
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
protected static function getUploadedFileMimeType(string $value, bool $useStorage)
|
||||
{
|
||||
if ($useStorage) {
|
||||
return Storage::mimeType($value);
|
||||
}
|
||||
|
||||
return File::mimeType($value);
|
||||
}
|
||||
}
|
||||
195
app/Models/Product.php
Normal file
195
app/Models/Product.php
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Casts\NutritionalValuesCast;
|
||||
use App\Lib\Helpers\FileStorageHelper;
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
||||
use Domain\Models\ValueObjects\Products\BreedingMethod;
|
||||
use Domain\Models\ValueObjects\Products\Business;
|
||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
||||
use Domain\Models\ValueObjects\Products\ProductFormat;
|
||||
use Domain\Models\ValueObjects\Products\Technology;
|
||||
use Illuminate\Database\Eloquent\Casts\AsEnumCollection;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Spatie\Translatable\HasTranslations;
|
||||
use Uwil\LaravelBackend\Interfaces\TranslatableModel;
|
||||
use Uwil\LaravelBackend\Traits\Models\HasActivityLog;
|
||||
use Uwil\LaravelBackend\Traits\Models\HasCache;
|
||||
use Uwil\LaravelBackend\Traits\Models\HasModelNaming;
|
||||
use Uwil\LaravelBackend\Traits\Models\HasUwilImages;
|
||||
|
||||
// phpcs:disable Generic.Files.LineLength.TooLong
|
||||
|
||||
/**
|
||||
* @property string $id
|
||||
* @property string $seo_title
|
||||
* @property string $seo_description
|
||||
* @property string $picture
|
||||
* @property string $picture_alt
|
||||
* @property string $picture_title
|
||||
* @property string $name
|
||||
* @property string $slug
|
||||
* @property ProductCategory $category Domain\Models\ValueObjects\Products\ProductRange
|
||||
* @property Technology $technology Domain\Models\ValueObjects\Products\Technology
|
||||
* @property bool $is_egalim
|
||||
* @property \Illuminate\Support\Collection<int, BreedingMethod> $breeding_methods Array of Domain\Models\ValueObjects\Products\BreedingMethod
|
||||
* @property \Illuminate\Support\Collection<int, Business> $businesses Array of Domain\Models\ValueObjects\Products\Business
|
||||
* @property \Illuminate\Support\Collection<int, ProductFormat> $formats Array of Domain\Models\ValueObjects\Products\ProductFormat
|
||||
* @property string $description
|
||||
* @property string $details_and_advantages
|
||||
* @property string $handling_advices
|
||||
* @property string|null $ingredients
|
||||
* @property string $conservation_before_opening
|
||||
* @property string|null $conservation_after_opening
|
||||
* @property \Domain\Models\ValueObjects\Products\NutritionalValues|null $nutritional_values Domain\Models\ValueObjects\NutritionalValues
|
||||
* @property string|null $pdf
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Uwil\LaravelBackend\Models\ActivityLog> $activityLogs
|
||||
* @property-read array $translatable_columns_from
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Recipe> $recipes
|
||||
* @property-read mixed $translations
|
||||
*
|
||||
* @method static \Database\Factories\ProductFactory factory($count = null, $state = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product whereJsonContainsLocale(string $column, string $locale, ?mixed $value, string $operand = '=')
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product whereJsonContainsLocales(string $column, array $locales, ?mixed $value, string $operand = '=')
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product whereLocale(string $column, string $locale)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product whereLocales(string $column, array $locales)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product withTrashed(bool $withTrashed = true)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product withoutTrashed()
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
// phpcs:enable Generic.Files.LineLength.TooLong
|
||||
class Product extends Model implements TranslatableModel
|
||||
{
|
||||
use HasActivityLog;
|
||||
use HasCache;
|
||||
|
||||
/** @use HasFactory<\Database\Factories\ProductFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use HasModelNaming;
|
||||
use HasTranslations;
|
||||
use HasUuids;
|
||||
use HasUwilImages;
|
||||
use Searchable;
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'seo_title',
|
||||
'seo_description',
|
||||
'picture',
|
||||
'picture_alt',
|
||||
'picture_title',
|
||||
'name',
|
||||
'category',
|
||||
'technology',
|
||||
'is_egalim',
|
||||
'breeding_methods',
|
||||
'businesses',
|
||||
'formats',
|
||||
'description',
|
||||
'details_and_advantages',
|
||||
'handling_advices',
|
||||
'ingredients',
|
||||
'conservation_before_opening',
|
||||
'conservation_after_opening',
|
||||
'nutritional_values',
|
||||
'pdf',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_egalim' => 'boolean',
|
||||
'category' => ProductCategory::class,
|
||||
'technology' => Technology::class,
|
||||
'breeding_methods' => AsEnumCollection::class . ':' . BreedingMethod::class,
|
||||
'businesses' => AsEnumCollection::class . ':' . Business::class,
|
||||
'formats' => AsEnumCollection::class . ':' . ProductFormat::class,
|
||||
'nutritional_values' => NutritionalValuesCast::class,
|
||||
];
|
||||
|
||||
public function newUniqueId(): string
|
||||
{
|
||||
return Uuid::uuid7()->toString();
|
||||
}
|
||||
|
||||
public function getId(): ProductId
|
||||
{
|
||||
return new ProductId(Uuid::fromString(strval($this->id)));
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (self $model): void {
|
||||
self::manageFields($model);
|
||||
});
|
||||
|
||||
static::deleting(function (self $model): void {
|
||||
FileStorageHelper::removeFile($model, 'picture');
|
||||
FileStorageHelper::removeFile($model, 'pdf');
|
||||
});
|
||||
}
|
||||
|
||||
/** @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\App\Models\Recipe,$this> */
|
||||
public function recipes(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Recipe::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relations should be share to views.
|
||||
*
|
||||
* @return array<int,string>
|
||||
*/
|
||||
public static function getPropsRelations(): array
|
||||
{
|
||||
return self::getCacheRelations();
|
||||
}
|
||||
|
||||
private static function manageFields(self $model): void
|
||||
{
|
||||
self::assertNameIsUnique($model->name, $model->id);
|
||||
|
||||
$model->slug = Str::slug($model->name);
|
||||
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
|
||||
if ($model->pdf) {
|
||||
$model->pdf = FileStorageHelper::storeFile($model, $model->pdf, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static function assertNameIsUnique(?string $name, string | int | null $id = null): void
|
||||
{
|
||||
$table = new self()->getTable();
|
||||
ToolboxHelper::assertFieldIsUnique($table, 'name', $name, $id);
|
||||
ToolboxHelper::assertFieldIsUnique($table, 'slug', Str::slug($name ?? ''), $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the indexable data array for the model.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'category' => $this->category?->value,
|
||||
];
|
||||
}
|
||||
}
|
||||
180
app/Models/Recipe.php
Normal file
180
app/Models/Recipe.php
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Lib\Helpers\FileStorageHelper;
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
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\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Spatie\Translatable\HasTranslations;
|
||||
use Uwil\LaravelBackend\Interfaces\TranslatableModel;
|
||||
use Uwil\LaravelBackend\Traits\Models\HasActivityLog;
|
||||
use Uwil\LaravelBackend\Traits\Models\HasCache;
|
||||
use Uwil\LaravelBackend\Traits\Models\HasModelNaming;
|
||||
use Uwil\LaravelBackend\Traits\Models\HasUwilImages;
|
||||
|
||||
// phpcs:disable Generic.Files.LineLength.TooLong
|
||||
|
||||
/**
|
||||
* @phpcs :disable Generic.Files.LineLength.TooLong
|
||||
*
|
||||
* @property string $id
|
||||
* @property string $seo_title
|
||||
* @property string $seo_description
|
||||
* @property string $picture
|
||||
* @property string $picture_alt
|
||||
* @property string $picture_title
|
||||
* @property string $name
|
||||
* @property string $slug
|
||||
* @property ProductCategory $product_category Domain\Models\ValueObjects\Products\ProductRange
|
||||
* @property ConsumptionTime $consomption_time Domain\Models\ValueObjects\Recipes\ConsumptionTime
|
||||
* @property Inspiration $inspiration Domain\Models\ValueObjects\Recipes\Inspiration
|
||||
* @property Season $season Domain\Models\ValueObjects\Recipes\Season
|
||||
* @property string|null $description
|
||||
* @property string|null $details
|
||||
* @property string $ingredients_list
|
||||
* @property string $preparation_steps
|
||||
* @property string|null $tips_and_advices
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Uwil\LaravelBackend\Models\ActivityLog> $activityLogs
|
||||
* @property-read array $translatable_columns_from
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Product> $products
|
||||
* @property-read mixed $translations
|
||||
*
|
||||
* @method static \Database\Factories\RecipeFactory factory($count = null, $state = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Recipe onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Recipe whereJsonContainsLocale(string $column, string $locale, ?mixed $value, string $operand = '=')
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Recipe whereJsonContainsLocales(string $column, array $locales, ?mixed $value, string $operand = '=')
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Recipe whereLocale(string $column, string $locale)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Recipe whereLocales(string $column, array $locales)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Recipe withTrashed(bool $withTrashed = true)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Recipe withoutTrashed()
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
// phpcs:enable Generic.Files.LineLength.TooLong
|
||||
class Recipe extends Model implements TranslatableModel
|
||||
{
|
||||
use HasActivityLog;
|
||||
use HasCache;
|
||||
|
||||
/** @use HasFactory<\Database\Factories\RecipeFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use HasModelNaming;
|
||||
use HasTranslations;
|
||||
use HasUuids;
|
||||
use HasUwilImages;
|
||||
use Searchable;
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'seo_title',
|
||||
'seo_description',
|
||||
'picture',
|
||||
'picture_alt',
|
||||
'picture_title',
|
||||
'name',
|
||||
'product_category',
|
||||
'consomption_time',
|
||||
'inspiration',
|
||||
'season',
|
||||
'description',
|
||||
'details',
|
||||
'ingredients_list',
|
||||
'preparation_steps',
|
||||
'tips_and_advices',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'product_category' => ProductCategory::class,
|
||||
'consomption_time' => ConsumptionTime::class,
|
||||
'inspiration' => Inspiration::class,
|
||||
'season' => Season::class,
|
||||
];
|
||||
|
||||
public function newUniqueId(): string
|
||||
{
|
||||
return Uuid::uuid7()->toString();
|
||||
}
|
||||
|
||||
public function getId(): RecipeId
|
||||
{
|
||||
return new RecipeId(Uuid::fromString($this->id));
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (self $model): void {
|
||||
self::manageFields($model);
|
||||
});
|
||||
|
||||
static::deleting(function (self $model): void {
|
||||
FileStorageHelper::removeFile($model, 'picture');
|
||||
});
|
||||
}
|
||||
|
||||
/** @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\App\Models\Product,$this> */
|
||||
public function products(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Product::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relations should be share to views.
|
||||
*
|
||||
* @return array<int,string>
|
||||
*/
|
||||
public static function getPropsRelations(): array
|
||||
{
|
||||
return self::getCacheRelations();
|
||||
}
|
||||
|
||||
private static function manageFields(self $model): void
|
||||
{
|
||||
self::assertNameIsUnique($model->name, $model->id);
|
||||
|
||||
$model->slug = Str::slug($model->name);
|
||||
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
|
||||
}
|
||||
|
||||
private static function assertNameIsUnique(?string $name, string | int | null $id = null): void
|
||||
{
|
||||
$table = new self()->getTable();
|
||||
ToolboxHelper::assertFieldIsUnique($table, 'name', $name, $id);
|
||||
ToolboxHelper::assertFieldIsUnique($table, 'slug', Str::slug($name ?? ''), $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the indexable data array for the model.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'product_category' => $this->product_category?->value,
|
||||
'consomption_time' => $this->consomption_time?->value,
|
||||
'inspiration' => $this->inspiration?->value,
|
||||
'season' => $this->season?->value,
|
||||
];
|
||||
}
|
||||
}
|
||||
58
app/Policies/ProductPolicy.php
Normal file
58
app/Policies/ProductPolicy.php
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Product;
|
||||
use Uwil\LaravelBackend\Enums\RoleEnum;
|
||||
use Uwil\LaravelBackend\Models\User;
|
||||
use Uwil\LaravelBackend\Policies\Rules\UserStaticRules;
|
||||
|
||||
class ProductPolicy
|
||||
{
|
||||
public function viewAny(User $authModel): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
|
||||
public function viewTrash(User $authModel): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
|
||||
public function view(User $authModel, Product $_): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
|
||||
public function create(User $authModel): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
|
||||
public function duplicate(User $authModel, Product $_): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
|
||||
public function update(User $authModel, Product $_): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
|
||||
public function delete(User $authModel, Product $_): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
|
||||
public function restore(User $authModel, Product $_): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
|
||||
public function forceDelete(User $authModel, Product $_): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
}
|
||||
58
app/Policies/RecipePolicy.php
Normal file
58
app/Policies/RecipePolicy.php
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Recipe;
|
||||
use Uwil\LaravelBackend\Enums\RoleEnum;
|
||||
use Uwil\LaravelBackend\Models\User;
|
||||
use Uwil\LaravelBackend\Policies\Rules\UserStaticRules;
|
||||
|
||||
class RecipePolicy
|
||||
{
|
||||
public function viewAny(User $authModel): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
|
||||
public function viewTrash(User $authModel): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
|
||||
public function view(User $authModel, Recipe $_): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
|
||||
public function create(User $authModel): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
|
||||
public function duplicate(User $authModel, Recipe $_): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
|
||||
public function update(User $authModel, Recipe $_): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
|
||||
public function delete(User $authModel, Recipe $_): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
|
||||
public function restore(User $authModel, Recipe $_): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
|
||||
public function forceDelete(User $authModel, Recipe $_): bool
|
||||
{
|
||||
return UserStaticRules::atLeastRole($authModel, RoleEnum::admin);
|
||||
}
|
||||
}
|
||||
162
app/Providers/AppServiceProvider.php
Normal file
162
app/Providers/AppServiceProvider.php
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Contracts\Directories\ProductDirectory;
|
||||
use App\Contracts\Directories\RecipeDirectory;
|
||||
use App\Directories\EloquentProductDirectory;
|
||||
use App\Directories\EloquentRecipeDirectory;
|
||||
use App\Enums\Contact\ActivitySectorEnum;
|
||||
use App\Enums\Contact\ObjectEnum;
|
||||
use App\Services\AdminAuthService;
|
||||
use Domain\Models\ValueObjects\Pagination;
|
||||
use Domain\Models\ValueObjects\Products\BreedingMethod;
|
||||
use Domain\Models\ValueObjects\Products\Business;
|
||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
||||
use Domain\Models\ValueObjects\Products\ProductFormat;
|
||||
use Domain\Models\ValueObjects\Products\Technology;
|
||||
use Domain\Models\ValueObjects\Recipes\ConsumptionTime;
|
||||
use Domain\Models\ValueObjects\Recipes\Inspiration;
|
||||
use Domain\Models\ValueObjects\Recipes\Season;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\Facades\Vite;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Uwil\LaravelBackend\Support\ModelListRegistry;
|
||||
use Uwil\LaravelCommon\Support\InertiaSharedRegistry;
|
||||
use Uwil\LaravelDynamicPages\Enums\PageStatusEnum;
|
||||
use Uwil\LaravelDynamicPages\Models\DynamicPage;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(
|
||||
AdminAuthService::class,
|
||||
function () {
|
||||
$guard = Auth::guard('backend');
|
||||
|
||||
return new AdminAuthService($guard);
|
||||
}
|
||||
);
|
||||
$this->app->singleton(
|
||||
ProductDirectory::class,
|
||||
fn () => new EloquentProductDirectory()
|
||||
);
|
||||
$this->app->singleton(
|
||||
RecipeDirectory::class,
|
||||
fn () => new EloquentRecipeDirectory()
|
||||
);
|
||||
|
||||
$this->requestMacro();
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->dateSetup();
|
||||
$this->laravelGlobalSetup();
|
||||
$this->registerSharedInertiaProps();
|
||||
$this->registerModelsInBackendRegistry();
|
||||
}
|
||||
|
||||
private function laravelGlobalSetup(): void
|
||||
{
|
||||
Model::shouldBeStrict(app()->environment('local'));
|
||||
|
||||
Vite::useCspNonce();
|
||||
|
||||
URL::forceScheme('https');
|
||||
}
|
||||
|
||||
private function registerSharedInertiaProps(): void
|
||||
{
|
||||
InertiaSharedRegistry::add('dynamicPages', function (): array {
|
||||
if (! Schema::hasTable('dynamic_pages')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$dynamicPageModels = DynamicPage::getModelsCached()
|
||||
->where('status', PageStatusEnum::published);
|
||||
|
||||
$dynamicPages = [];
|
||||
foreach ($dynamicPageModels as $dynamicPageModel) {
|
||||
$pascalCase = str_replace(' ', '', ucwords(str_replace('-', ' ', $dynamicPageModel->id_key)));
|
||||
$dynamicPages["page{$pascalCase}"] = $dynamicPageModel;
|
||||
}
|
||||
|
||||
return $dynamicPages;
|
||||
});
|
||||
|
||||
InertiaSharedRegistry::add('activitySectorEnum', fn () => ActivitySectorEnum::toArray());
|
||||
InertiaSharedRegistry::add('objectEnum', fn () => ObjectEnum::toArray());
|
||||
InertiaSharedRegistry::add('breedingMethodEnum', fn () => BreedingMethod::toArray());
|
||||
InertiaSharedRegistry::add('businessEnum', fn () => Business::toArray());
|
||||
InertiaSharedRegistry::add('productCategoryEnum', fn () => ProductCategory::toArray());
|
||||
InertiaSharedRegistry::add('productFormatEnum', fn () => ProductFormat::toArray());
|
||||
InertiaSharedRegistry::add('technologyEnum', fn () => Technology::toArray());
|
||||
InertiaSharedRegistry::add('consumptionTimeEnum', fn () => ConsumptionTime::toArray());
|
||||
InertiaSharedRegistry::add('inspirationEnum', fn () => Inspiration::toArray());
|
||||
InertiaSharedRegistry::add('seasonEnum', fn () => Season::toArray());
|
||||
}
|
||||
|
||||
private function registerModelsInBackendRegistry(): void
|
||||
{
|
||||
ModelListRegistry::add(
|
||||
2,
|
||||
'products',
|
||||
trans_choice('models.classes.product', 1),
|
||||
'name',
|
||||
);
|
||||
ModelListRegistry::add(
|
||||
3,
|
||||
'recipes',
|
||||
trans_choice('models.classes.recipe', 1),
|
||||
'name',
|
||||
);
|
||||
}
|
||||
|
||||
private function dateSetup(): void
|
||||
{
|
||||
foreach ([\LC_TIME, \LC_MESSAGES, \LC_COLLATE, \LC_CTYPE, \LC_MONETARY] as $constant) {
|
||||
setlocale(
|
||||
$constant,
|
||||
sprintf(
|
||||
'%s_%s.UTF-8',
|
||||
Config::string('app.locale'),
|
||||
strtoupper(Config::string('app.locale'))
|
||||
)
|
||||
);
|
||||
}
|
||||
Date::setLocale(Config::string('app.locale'));
|
||||
Date::setFallbackLocale(Config::string('app.fallback_locale'));
|
||||
}
|
||||
|
||||
private function requestMacro(): void
|
||||
{
|
||||
Request::macro('pagination', function (int $defaultPageSize = 25): Pagination {
|
||||
/** @var \Illuminate\Http\Request $this */
|
||||
if ($this->has('offset') || $this->has('limit')) {
|
||||
return Pagination::ofOffsetAndLimit(
|
||||
$this->integer('offset', 0),
|
||||
$this->integer('limit', $defaultPageSize)
|
||||
);
|
||||
}
|
||||
|
||||
return Pagination::ofPage(
|
||||
$this->integer('page', 1),
|
||||
$this->integer(
|
||||
'per_page',
|
||||
// * Default laravel paginator name
|
||||
$this->integer('pagination', $defaultPageSize)
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
41
app/Providers/AuthServiceProvider.php
Normal file
41
app/Providers/AuthServiceProvider.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\Recipe;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Str;
|
||||
use Uwil\LaravelCommon\Support\InertiaSharedRegistry;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
InertiaSharedRegistry::add('auth.policies', function () {
|
||||
$localModels = [
|
||||
Product::class,
|
||||
Recipe::class,
|
||||
];
|
||||
|
||||
$localPolicies = [];
|
||||
foreach ($localModels as $model) {
|
||||
$key = Str::camel(Str::plural(class_basename($model)));
|
||||
|
||||
$localPolicies[$key] = [
|
||||
'viewAny' => Gate::allows('viewAny', $model),
|
||||
'create' => Gate::allows('create', $model),
|
||||
'viewTrash' => Gate::allows('viewTrash', $model),
|
||||
];
|
||||
}
|
||||
|
||||
return $localPolicies;
|
||||
});
|
||||
}
|
||||
}
|
||||
89
app/Providers/RouteServiceProvider.php
Normal file
89
app/Providers/RouteServiceProvider.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Contracts\Directories\ProductDirectory;
|
||||
use App\Contracts\Directories\RecipeDirectory;
|
||||
use App\Models\Product;
|
||||
use App\Models\Recipe;
|
||||
use Domain\Models\Entities\Product as DomainProduct;
|
||||
use Domain\Models\Entities\Recipe as DomainRecipe;
|
||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
||||
use Domain\Models\ValueObjects\Entities\RecipeId;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Str;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\Helpers\UuidHelper;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Define your route model bindings, pattern filters, and other route configuration.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Route::bind('product', function (string $value) {
|
||||
if (request()->is('back/*') || request()->is('back') || request()->routeIs('back.*')) {
|
||||
return Product::withTrashed()
|
||||
->where('id', $value)
|
||||
->orWhere('slug', $value)
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
return $this->resolveProduct($value);
|
||||
});
|
||||
|
||||
Route::bind('recipe', function (string $value) {
|
||||
if (request()->is('back/*') || request()->is('back') || request()->routeIs('back.*')) {
|
||||
return Recipe::withTrashed()
|
||||
->where('id', $value)
|
||||
->orWhere('slug', $value)
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
return $this->resolveRecipe($value);
|
||||
});
|
||||
}
|
||||
|
||||
private function resolveProduct(string $value): DomainProduct
|
||||
{
|
||||
$productDirectory = app(ProductDirectory::class);
|
||||
|
||||
if (Str::isUuid($value) && UuidHelper::isV7(Uuid::fromString($value))) {
|
||||
$product = $productDirectory->find(new ProductId(Uuid::fromString($value)));
|
||||
}
|
||||
|
||||
if (! isset($product)) {
|
||||
$product = $productDirectory->findBySlug($value);
|
||||
}
|
||||
|
||||
if (! isset($product)) {
|
||||
throw new ModelNotFoundException('Product not found');
|
||||
}
|
||||
|
||||
return $product;
|
||||
}
|
||||
|
||||
private function resolveRecipe(string $value): DomainRecipe
|
||||
{
|
||||
$recipeDirectory = app(RecipeDirectory::class);
|
||||
|
||||
if (Str::isUuid($value) && UuidHelper::isV7(Uuid::fromString($value))) {
|
||||
$recipe = $recipeDirectory->find(new RecipeId(Uuid::fromString($value)));
|
||||
}
|
||||
|
||||
if (! isset($recipe)) {
|
||||
$recipe = $recipeDirectory->findBySlug($value);
|
||||
}
|
||||
|
||||
if (! isset($recipe)) {
|
||||
throw new ModelNotFoundException('Recipe not found');
|
||||
}
|
||||
|
||||
return $recipe;
|
||||
}
|
||||
}
|
||||
24
app/Providers/ViewServiceProvider.php
Normal file
24
app/Providers/ViewServiceProvider.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Contracts\View\View as ContractsView;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\Facades\Vite;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class ViewServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
if ($this->app->runningInConsole()) {
|
||||
return;
|
||||
}
|
||||
|
||||
View::composer('*', function (ContractsView $view) {
|
||||
$view->with('csp_nonce', Vite::cspNonce());
|
||||
});
|
||||
}
|
||||
}
|
||||
59
app/Services/AdminAuthService.php
Normal file
59
app/Services/AdminAuthService.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Exceptions\AuthServiceException;
|
||||
use Illuminate\Contracts\Auth\StatefulGuard;
|
||||
use Uwil\LaravelBackend\Models\User;
|
||||
|
||||
class AdminAuthService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly StatefulGuard $guard
|
||||
) {
|
||||
}
|
||||
|
||||
public function canWithUser(\UnitEnum | string $abilities, mixed $arguments = []): bool
|
||||
{
|
||||
return $this->getUser()?->can($abilities, $arguments) ?? false;
|
||||
}
|
||||
|
||||
public function check(): bool
|
||||
{
|
||||
return $this->guard->check();
|
||||
}
|
||||
|
||||
public function guest(): bool
|
||||
{
|
||||
return ! $this->check();
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
$user = $this->guard->user();
|
||||
|
||||
if ($user && ! ($user instanceof User)) {
|
||||
throw new \InvalidArgumentException('Auth service needs a backend guard');
|
||||
}
|
||||
|
||||
return $user ? clone $user : null;
|
||||
}
|
||||
|
||||
public function getUserAsMandatory(): User
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
if ($user === null) {
|
||||
throw new AuthServiceException('Authenticated user is required from here');
|
||||
}
|
||||
|
||||
return clone $user;
|
||||
}
|
||||
|
||||
public function getMandatoryUserId(): string
|
||||
{
|
||||
return (string) $this->getUserAsMandatory()->id;
|
||||
}
|
||||
}
|
||||
18
artisan
Normal file
18
artisan
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
13
bootstrap/app.php
Normal file
13
bootstrap/app.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
health: '/up',
|
||||
)
|
||||
->withProviders(require __DIR__ . '/providers.php')
|
||||
->withMiddleware(require __DIR__ . '/middlewares.php')
|
||||
->withExceptions(require __DIR__ . '/exceptions.php')->create();
|
||||
2
bootstrap/cache/.gitignore
vendored
Normal file
2
bootstrap/cache/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
45
bootstrap/exceptions.php
Normal file
45
bootstrap/exceptions.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Uwil\LaravelCommon\Helpers\InertiaHelper;
|
||||
|
||||
return function (Exceptions $exceptions) {
|
||||
$exceptions->render(function (HttpException $e, Request $request) {
|
||||
$status = $e->getStatusCode();
|
||||
|
||||
if ($request->isAttemptingPrecognition() && $status === 204) {
|
||||
return response('', 204, $e->getHeaders());
|
||||
}
|
||||
|
||||
return InertiaHelper::errorRender($request, $status);
|
||||
});
|
||||
|
||||
$exceptions->report(function (\Throwable $e) {
|
||||
if (app()->bound('sentry')) {
|
||||
app('sentry')->captureException($e);
|
||||
}
|
||||
});
|
||||
|
||||
$exceptions->render(function (ValidationException $e, Request $request) {
|
||||
if ($request->wantsJson() || $request->isAttemptingPrecognition()) {
|
||||
return response()->json([
|
||||
'message' => $e->getMessage(),
|
||||
'errors' => $e->errors(),
|
||||
], $e->status);
|
||||
}
|
||||
|
||||
$safeInput = Collection::make($request->all())
|
||||
->except(['password', 'password_confirmation', 'current_password'])
|
||||
->toArray();
|
||||
|
||||
return back()
|
||||
->withInput($safeInput)
|
||||
->withErrors($e->errors());
|
||||
});
|
||||
};
|
||||
89
bootstrap/middlewares.php
Normal file
89
bootstrap/middlewares.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Http\Middlewares\HtmlSanitizer;
|
||||
use App\Http\Middlewares\SecureHeaders;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Uwil\AdminSecure\Laravel\Http\Middleware\AdminSecureMiddleware;
|
||||
use Uwil\LaravelBackend\Middlewares\Authenticate;
|
||||
use Uwil\LaravelBackend\Middlewares\EncryptCookies;
|
||||
use Uwil\LaravelBackend\Middlewares\LogoutUnpublishedUsers;
|
||||
use Uwil\LaravelBackend\Middlewares\RedirectIfAuthenticated;
|
||||
use Uwil\LaravelCommon\Middlewares\DisableSsr;
|
||||
use Uwil\LaravelCommon\Middlewares\ForceLocale;
|
||||
use Uwil\LaravelCommon\Middlewares\HandleInertiaRequests;
|
||||
use Uwil\LaravelCommon\Middlewares\TrimStrings;
|
||||
use Uwil\LaravelCommon\Middlewares\TrustHosts;
|
||||
use Uwil\LaravelCommon\Middlewares\TrustProxies;
|
||||
|
||||
$redirectGuests = function (Request $request) {
|
||||
$logins = [
|
||||
'backend' => route('back.login'),
|
||||
'frontend' => '/',
|
||||
];
|
||||
|
||||
foreach ($request->route()?->gatherMiddleware() ?? [] as $middleware) {
|
||||
if (\is_string($middleware) && str_contains($middleware, 'auth:')) {
|
||||
$route = $logins[str_replace('auth:', '', $middleware)] ?? null;
|
||||
if ($route) {
|
||||
return $route;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $logins['frontend'];
|
||||
};
|
||||
|
||||
$redirectUsers = function (Request $request) {
|
||||
$logins = [
|
||||
'backend' => route('back.dashboard.index'),
|
||||
'frontend' => '/',
|
||||
];
|
||||
foreach ($request->route()?->gatherMiddleware() ?? [] as $middleware) {
|
||||
if (\is_string($middleware) && str_contains($middleware, 'auth:')) {
|
||||
$route = $logins[str_replace('auth:', '', $middleware)] ?? null;
|
||||
if ($route) {
|
||||
return $route;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $logins['frontend'];
|
||||
};
|
||||
|
||||
return function (Middleware $middleware) use ($redirectGuests, $redirectUsers): void {
|
||||
$middleware->trustProxies(at: '*');
|
||||
$middleware->redirectGuestsTo($redirectGuests);
|
||||
$middleware->redirectUsersTo($redirectUsers);
|
||||
|
||||
$middleware->web(append: [
|
||||
ForceLocale::class,
|
||||
HandleInertiaRequests::class,
|
||||
AddLinkHeadersForPreloadedAssets::class,
|
||||
SecureHeaders::class,
|
||||
TrimStrings::class,
|
||||
TrustHosts::class,
|
||||
TrustProxies::class,
|
||||
LogoutUnpublishedUsers::class,
|
||||
]);
|
||||
$middleware->preventRequestForgery(except: [
|
||||
'/csp-report',
|
||||
]);
|
||||
$middleware->group('saml', [
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
]);
|
||||
$middleware->alias([
|
||||
'auth' => Authenticate::class,
|
||||
'guest' => RedirectIfAuthenticated::class,
|
||||
'htmlsanitize' => HtmlSanitizer::class,
|
||||
'admin-secure' => AdminSecureMiddleware::class,
|
||||
'disable-ssr' => DisableSsr::class,
|
||||
]);
|
||||
};
|
||||
27
bootstrap/providers.php
Normal file
27
bootstrap/providers.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Providers\AppServiceProvider;
|
||||
use App\Providers\AuthServiceProvider;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use App\Providers\ViewServiceProvider;
|
||||
use Uwil\AdminSecure\Laravel\AdminSecureServiceProvider;
|
||||
use Uwil\LaravelBackend\BackendServiceProvider;
|
||||
use Uwil\LaravelCommon\CommonServiceProvider;
|
||||
use Uwil\LaravelDynamicPages\DynamicPagesServiceProvider;
|
||||
use Uwil\LaravelK8S\K8SServiceProvider;
|
||||
use Uwil\LaravelSchemaOrg\SchemaOrgServiceProvider;
|
||||
|
||||
return [
|
||||
AppServiceProvider::class,
|
||||
AuthServiceProvider::class,
|
||||
RouteServiceProvider::class,
|
||||
ViewServiceProvider::class,
|
||||
AdminSecureServiceProvider::class,
|
||||
BackendServiceProvider::class,
|
||||
CommonServiceProvider::class,
|
||||
DynamicPagesServiceProvider::class,
|
||||
K8SServiceProvider::class,
|
||||
SchemaOrgServiceProvider::class,
|
||||
];
|
||||
1
commitlint.config.js
Normal file
1
commitlint.config.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default { extends: ["@uwil/husky-config/commitlint.config.js"] };
|
||||
21
components.json
Normal file
21
components.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "resources/sass/common.scss",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
124
composer.json
Normal file
124
composer.json
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "uwil/laravel-project",
|
||||
"type": "project",
|
||||
"description": "UWIL Laravel project",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"framework"
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.5",
|
||||
"scaler-tech/laravel-saml2": "^2.5",
|
||||
"diglactic/laravel-breadcrumbs": "^10.0",
|
||||
"inertiajs/inertia-laravel": "^3.0",
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/scout": "^11.3",
|
||||
"laravel/telescope": "^5.7",
|
||||
"laravel/tinker": "^3.0",
|
||||
"meilisearch/meilisearch-php": "^1.16",
|
||||
"monolog/monolog": "^3.0",
|
||||
"psr/log": "^3.0",
|
||||
"sentry/sentry-laravel": "^4.25",
|
||||
"tightenco/ziggy": "^2.5",
|
||||
"uwil/k8s-laravel": "^3.0",
|
||||
"uwil/admin-secure-laravel": "^6.0",
|
||||
"uwil/laravel-dynamic-pages": "^5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-ide-helper": "^3.7",
|
||||
"fakerphp/faker": "^1.23",
|
||||
"larastan/larastan": "^3.4",
|
||||
"laravel-lang/common": "^6.8",
|
||||
"laravel/breeze": "^2.3.0",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.29",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.9",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.4",
|
||||
"phpunit/phpunit": "^12.0",
|
||||
"slevomat/coding-standard": "^8.22"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/",
|
||||
"Domain\\": "domain/",
|
||||
"Infrastructure\\": "infra/",
|
||||
"Shared\\": "shared/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"setup": [
|
||||
"composer install",
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||
"@php artisan key:generate",
|
||||
"@php artisan migrate --force",
|
||||
"npm install",
|
||||
"npm run build"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
|
||||
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
|
||||
"@php artisan lang:update",
|
||||
"@php artisan ide-helper:generate",
|
||||
"@php artisan ide-helper:models -RW",
|
||||
"./vendor/bin/pint app/Models"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
],
|
||||
"pre-package-uninstall": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true,
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||
},
|
||||
"platform-check": false
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"repositories": [
|
||||
{
|
||||
"name": "lab.s-web.fr",
|
||||
"type": "composer",
|
||||
"url": "https://lab.s-web.fr/api/v4/group/23/-/packages/composer/packages.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
13414
composer.lock
generated
Normal file
13414
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
112
config/app.php
Normal file
112
config/app.php
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Application Name
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This value is the name of your application, which will be used when the
|
||||
* framework needs to place the application's name in a notification or
|
||||
* other UI elements where an application name needs to be displayed.
|
||||
*/
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Application Environment
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This value determines the "environment" your application is currently
|
||||
* running in. This may determine how you prefer to configure various
|
||||
* services the application utilizes. Set this in your ".env" file.
|
||||
*/
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Application Debug Mode
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* When your application is in debug mode, detailed error messages with
|
||||
* stack traces will be shown on every error that occurs within your
|
||||
* application. If disabled, a simple generic error page is shown.
|
||||
*/
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Application URL
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This URL is used by the console to properly generate URLs when using
|
||||
* the Artisan command line tool. You should set this to the root of
|
||||
* the application so that it's available within Artisan commands.
|
||||
*/
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Application Timezone
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Here you may specify the default timezone for your application, which
|
||||
* will be used by the PHP date and date-time functions. The timezone
|
||||
* is set to "UTC" by default as it is suitable for most use cases.
|
||||
*/
|
||||
'timezone' => env('APP_TIMEZONE', 'Europe/Paris'),
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Application Locale Configuration
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* The application locale determines the default locale that will be used
|
||||
* by Laravel's translation / localization methods. This option can be
|
||||
* set to any locale for which you plan to have translation strings.
|
||||
*/
|
||||
'locale' => env('APP_LOCALE', 'fr'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'fr'),
|
||||
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'fr_FR'),
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Encryption Key
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This key is utilized by Laravel's encryption services and should be set
|
||||
* to a random, 32 character string to ensure that all encrypted values
|
||||
* are secure. You should do this prior to deploying the application.
|
||||
*/
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Maintenance Mode Driver
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* These configuration options determine the driver used to determine and
|
||||
* manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
* allow maintenance mode to be controlled across multiple machines.
|
||||
*
|
||||
* Supported drivers: "file", "cache"
|
||||
*/
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
];
|
||||
109
config/auth.php
Normal file
109
config/auth.php
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Uwil\LaravelBackend\Models\User;
|
||||
|
||||
return [
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Authentication Defaults
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This option defines the default authentication "guard" and password
|
||||
* reset "broker" for your application. You may change these values
|
||||
* as required, but they're a perfect start for most applications.
|
||||
*/
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Authentication Guards
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Next, you may define every authentication guard for your application.
|
||||
* Of course, a great default configuration has been defined for you
|
||||
* which utilizes session storage plus the Eloquent user provider.
|
||||
*
|
||||
* All authentication guards have a user provider, which defines how the
|
||||
* users are actually retrieved out of your database or other storage
|
||||
* system used by the application. Typically, Eloquent is utilized.
|
||||
*
|
||||
* Supported: "session"
|
||||
*/
|
||||
'guards' => [
|
||||
'backend' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
|
||||
'frontend' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'customers',
|
||||
],
|
||||
],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* User Providers
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* All authentication guards have a user provider, which defines how the
|
||||
* users are actually retrieved out of your database or other storage
|
||||
* system used by the application. Typically, Eloquent is utilized.
|
||||
*
|
||||
* If you have multiple user tables or models you may configure multiple
|
||||
* providers to represent the model / table. These providers may then
|
||||
* be assigned to any extra authentication guards you have defined.
|
||||
*
|
||||
* Supported: "database", "eloquent"
|
||||
*/
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', User::class),
|
||||
],
|
||||
],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Resetting Passwords
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* These configuration options specify the behavior of Laravel's password
|
||||
* reset functionality, including the table utilized for token storage
|
||||
* and the user provider that is invoked to actually retrieve users.
|
||||
*
|
||||
* The expiry time is the number of minutes that each reset token will be
|
||||
* considered valid. This security feature keeps tokens short-lived so
|
||||
* they have less time to be guessed. You may change this as needed.
|
||||
*
|
||||
* The throttle setting is the number of seconds a user must wait before
|
||||
* generating more password reset tokens. This prevents the user from
|
||||
* quickly generating a very large amount of password reset tokens.
|
||||
*/
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Password Confirmation Timeout
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Here you may define the amount of seconds before a password confirmation
|
||||
* window expires and users are asked to re-enter their password via the
|
||||
* confirmation screen. By default, the timeout lasts for three hours.
|
||||
*/
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
|
||||
];
|
||||
76
config/breadcrumbs.php
Normal file
76
config/breadcrumbs.php
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Diglactic\Breadcrumbs\Generator;
|
||||
use Diglactic\Breadcrumbs\Manager;
|
||||
|
||||
return [
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* View Name
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Choose a view to display when Breadcrumbs::render() is called.
|
||||
* Built in templates are:
|
||||
*
|
||||
* - 'breadcrumbs::bootstrap5' - Bootstrap 5
|
||||
* - 'breadcrumbs::bootstrap4' - Bootstrap 4
|
||||
* - 'breadcrumbs::bulma' - Bulma
|
||||
* - 'breadcrumbs::foundation6' - Foundation 6
|
||||
* - 'breadcrumbs::json-ld' - JSON-LD Structured Data
|
||||
* - 'breadcrumbs::materialize' - Materialize
|
||||
* - 'breadcrumbs::tailwind' - Tailwind CSS
|
||||
* - 'breadcrumbs::uikit' - UIkit
|
||||
*
|
||||
* Or a custom view, e.g. '_partials/breadcrumbs'.
|
||||
*/
|
||||
'view' => null,
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Breadcrumbs File(s)
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* The file(s) where breadcrumbs are defined. e.g.
|
||||
*
|
||||
* - base_path('routes/breadcrumbs.php')
|
||||
* - glob(base_path('breadcrumbs/*.php'))
|
||||
*/
|
||||
'files' => array_filter([
|
||||
...glob(base_path('routes/breadcrumbs/*.php')) ?: [],
|
||||
]),
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Exceptions
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Determine when to throw an exception.
|
||||
*/
|
||||
|
||||
// * When route-bound breadcrumbs are used but the current route doesn't have a name (UnnamedRouteException)
|
||||
'unnamed-route-exception' => true,
|
||||
|
||||
// * When route-bound breadcrumbs are used and the matching breadcrumb doesn't exist (InvalidBreadcrumbException)
|
||||
'missing-route-bound-breadcrumb-exception' => true,
|
||||
|
||||
// * When a named breadcrumb is used but doesn't exist (InvalidBreadcrumbException)
|
||||
'invalid-named-breadcrumb-exception' => true,
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Classes
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Subclass the default classes for more advanced customisations.
|
||||
*/
|
||||
|
||||
// * Manager
|
||||
'manager-class' => Manager::class,
|
||||
|
||||
// * Generator
|
||||
'generator-class' => Generator::class,
|
||||
|
||||
];
|
||||
117
config/cache.php
Normal file
117
config/cache.php
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Default Cache Store
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This option controls the default cache store that will be used by the
|
||||
* framework. This connection is utilized if another isn't explicitly
|
||||
* specified when running a cache operation inside the application.
|
||||
*
|
||||
*/
|
||||
|
||||
'default' => env('CACHE_STORE', 'database'),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Cache Stores
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Here you may define all of the cache "stores" for your application as
|
||||
* well as their drivers. You may even define multiple stores for the
|
||||
* same cache driver to group types of items stored in your caches.
|
||||
*
|
||||
* Supported drivers: "array", "database", "file", "memcached",
|
||||
* "redis", "dynamodb", "octane",
|
||||
* "failover", "null"
|
||||
*
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
'path' => storage_path('framework/cache/data'),
|
||||
'lock_path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
'sasl' => [
|
||||
env('MEMCACHED_USERNAME'),
|
||||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [],
|
||||
'servers' => [
|
||||
[
|
||||
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||
'port' => env('MEMCACHED_PORT', 11211),
|
||||
'weight' => 100,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||
],
|
||||
|
||||
'dynamodb' => [
|
||||
'driver' => 'dynamodb',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||
],
|
||||
|
||||
'octane' => [
|
||||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'stores' => [
|
||||
'database',
|
||||
'array',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Cache Key Prefix
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||
* stores, there might be other applications using the same cache. For
|
||||
* that reason, you may prefix every cache key to avoid collisions.
|
||||
*
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')) . '-cache-'),
|
||||
|
||||
];
|
||||
21
config/cookie-notice.php
Normal file
21
config/cookie-notice.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'google' => [
|
||||
'recaptcha' => [
|
||||
'public' => env('GOOGLE_RECAPTCHA_PUBLIC'),
|
||||
'private' => env('GOOGLE_RECAPTCHA_PRIVATE'),
|
||||
'version' => env('GOOGLE_RECAPTCHA_VERSION', '3') === '3' ? true : false,
|
||||
'bypassKey' => 'bypass',
|
||||
],
|
||||
'analytics' => [
|
||||
'id' => env('GOOGLE_ANALYTICS_ID'),
|
||||
],
|
||||
],
|
||||
|
||||
'matomo' => [
|
||||
'site_id' => env('MATOMO_SITE_ID'),
|
||||
],
|
||||
];
|
||||
190
config/database.php
Normal file
190
config/database.php
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Pdo\Mysql;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Default Database Connection Name
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Here you may specify which of the database connections below you wish
|
||||
* to use as your default connection for database operations. This is
|
||||
* the connection which will be utilized unless another connection
|
||||
* is explicitly specified when you execute a query / statement.
|
||||
*
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Database Connections
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Below are all of the database connections defined for your application.
|
||||
* An example configuration is provided for each database system which
|
||||
* is supported by Laravel. You're free to add / remove connections.
|
||||
*
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
'transaction_mode' => 'DEFERRED',
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500
|
||||
? Mysql::ATTR_SSL_CA
|
||||
: \PDO::MYSQL_ATTR_SSL_CA
|
||||
) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
'driver' => 'mariadb',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500
|
||||
? Mysql::ATTR_SSL_CA
|
||||
: \PDO::MYSQL_ATTR_SSL_CA
|
||||
) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'port' => env('DB_PORT', '1433'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Migration Repository Table
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This table keeps track of all the migrations that have already run for
|
||||
* your application. Using this information, we can determine which of
|
||||
* the migrations on disk haven't actually been run on the database.
|
||||
*
|
||||
*/
|
||||
|
||||
'migrations' => [
|
||||
'table' => 'migrations',
|
||||
'update_date_on_publish' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Redis Databases
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Redis is an open source, fast, and advanced key-value store that also
|
||||
* provides a richer body of commands than a typical key-value system
|
||||
* such as Memcached. You may define your connection settings here.
|
||||
*
|
||||
*/
|
||||
|
||||
'redis' => [
|
||||
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')) . '-database-'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
76
config/filesystems.php
Normal file
76
config/filesystems.php
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Default Filesystem Disk
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Here you may specify the default filesystem disk that should be used
|
||||
* by the framework. The "local" disk, as well as a variety of cloud
|
||||
* based disks are available to your application for file storage.
|
||||
*/
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Filesystem Disks
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Below you may configure as many filesystem disks as necessary, and you
|
||||
* may even configure multiple disks for the same driver. Examples for
|
||||
* most supported storage drivers are configured here for reference.
|
||||
*
|
||||
* Supported drivers: "local", "ftp", "sftp", "s3"
|
||||
*/
|
||||
'disks' => [
|
||||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app'),
|
||||
'serve' => true,
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL') . '/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Symbolic Links
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Here you may configure the symbolic links that will be created when the
|
||||
* `storage:link` Artisan command is executed. The array keys should be
|
||||
* the locations of the links and the values should be their targets.
|
||||
*/
|
||||
'links' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
|
||||
];
|
||||
379
config/ide-helper.php
Normal file
379
config/ide-helper.php
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Client\Factory;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Session\Store;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filename
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The default filename.
|
||||
|
|
||||
*/
|
||||
|
||||
'filename' => '_ide_helper.php',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Models filename
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The default filename for the models helper file.
|
||||
|
|
||||
*/
|
||||
|
||||
'models_filename' => '_ide_helper_models.php',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| PhpStorm meta filename
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| PhpStorm also supports the directory `.phpstorm.meta.php/` with arbitrary
|
||||
| files in it, should you need additional files for your project; e.g.
|
||||
| `.phpstorm.meta.php/laravel_ide_Helper.php'.
|
||||
|
|
||||
*/
|
||||
'meta_filename' => '.phpstorm.meta.php',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fluent helpers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set to true to generate commonly used Fluent methods.
|
||||
|
|
||||
*/
|
||||
|
||||
'include_fluent' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Write model query methods
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set to false to disable generated docs for the 'query()', 'newQuery()' and 'newModelQuery()' methods.
|
||||
|
|
||||
*/
|
||||
|
||||
'write_query_methods' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Write model magic methods
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set to false to disable write magic methods of model.
|
||||
|
|
||||
*/
|
||||
|
||||
'write_model_magic_where' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Write model external Eloquent builder methods
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set to false to disable write external Eloquent builder methods.
|
||||
|
|
||||
*/
|
||||
|
||||
'write_model_external_builder_methods' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Write model relation count and exists properties
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set to false to disable writing of relation count and exists properties
|
||||
| to model DocBlocks.
|
||||
|
|
||||
*/
|
||||
|
||||
'write_model_relation_count_properties' => false,
|
||||
'write_model_relation_exists_properties' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Write Eloquent model mixins
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This will add the necessary DocBlock mixins to the model class
|
||||
| contained in the Laravel framework. This helps the IDE with
|
||||
| auto-completion.
|
||||
|
|
||||
| Please be aware that this setting changes a file within the /vendor directory.
|
||||
|
|
||||
*/
|
||||
|
||||
'write_eloquent_model_mixins' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Helper files to include
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Include helper files. By default not included, but can be toggled with the
|
||||
| -- helpers (-H) option. Extra helper files can be included.
|
||||
|
|
||||
*/
|
||||
|
||||
'include_helpers' => false,
|
||||
|
||||
'helper_files' => [
|
||||
base_path() . '/vendor/laravel/framework/src/Illuminate/Support/helpers.php',
|
||||
base_path() . '/vendor/laravel/framework/src/Illuminate/Foundation/helpers.php',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Model locations to include
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define in which directories the ide-helper:models command should look
|
||||
| for models.
|
||||
|
|
||||
| glob patterns are supported to easier reach models in sub-directories,
|
||||
| e.g. `app/Services/* /Models` (without the space).
|
||||
|
|
||||
*/
|
||||
|
||||
'model_locations' => [
|
||||
'app',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Models to ignore
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define which models should be ignored.
|
||||
|
|
||||
*/
|
||||
|
||||
'ignored_models' => [],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Models hooks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define which hook classes you want to run for models to add custom information.
|
||||
|
|
||||
| Hooks should implement Barryvdh\LaravelIdeHelper\Contracts\ModelHookInterface.
|
||||
|
|
||||
*/
|
||||
|
||||
'model_hooks' => [
|
||||
// App\Support\IdeHelper\MyModelHook::class
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Extra classes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These implementations are not really extended, but called with magic functions.
|
||||
|
|
||||
*/
|
||||
|
||||
'extra' => [
|
||||
'Eloquent' => [Builder::class, \Illuminate\Database\Query\Builder::class],
|
||||
'Session' => [Store::class],
|
||||
],
|
||||
|
||||
'magic' => [],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Interface implementations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These interfaces will be replaced with the implementing class. Some interfaces
|
||||
| are detected by the helpers, others can be listed below.
|
||||
|
|
||||
*/
|
||||
|
||||
'interfaces' => [
|
||||
// App\MyInterface::class => App\MyImplementation::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Support for camel cased models
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| There are some Laravel packages (such as Eloquence) that allow for accessing
|
||||
| Eloquent model properties via camel case, instead of snake case.
|
||||
|
|
||||
| Enabling this option will support these packages by saving all model
|
||||
| properties as camel case, instead of snake case.
|
||||
|
|
||||
| For example, normally you would see this:
|
||||
|
|
||||
| * @property \Illuminate\Support\Carbon $created_at
|
||||
| * @property \Illuminate\Support\Carbon $updated_at
|
||||
|
|
||||
| With this enabled, the properties will be this:
|
||||
|
|
||||
| * @property \Illuminate\Support\Carbon $createdAt
|
||||
| * @property \Illuminate\Support\Carbon $updatedAt
|
||||
|
|
||||
| Note, it is currently an all-or-nothing option.
|
||||
|
|
||||
*/
|
||||
'model_camel_case_properties' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Property casts
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Cast the given "real type" to the given "type".
|
||||
|
|
||||
*/
|
||||
'type_overrides' => [
|
||||
'integer' => 'int',
|
||||
'boolean' => 'bool',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Include DocBlocks from classes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Include DocBlocks from classes to allow additional code inspection for
|
||||
| magic methods and properties.
|
||||
|
|
||||
*/
|
||||
'include_class_docblocks' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Force FQN usage
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Use the fully qualified (class) name in DocBlocks,
|
||||
| even if the class exists in the same namespace
|
||||
| or there is an import (use className) of the class.
|
||||
|
|
||||
*/
|
||||
'force_fqn' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Use generics syntax
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Use generics syntax within DocBlocks,
|
||||
| e.g. `Collection<User>` instead of `Collection|User[]`.
|
||||
|
|
||||
*/
|
||||
'use_generics_annotations' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default return types for macros
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define default return types for macros without explicit return types.
|
||||
| e.g. `\Illuminate\Database\Query\Builder::class => 'static'`,
|
||||
| `\Illuminate\Support\Str::class => 'string'`
|
||||
|
|
||||
*/
|
||||
'macro_default_return_types' => [
|
||||
Factory::class => PendingRequest::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Additional relation types
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Sometimes it's needed to create custom relation types. The key of the array
|
||||
| is the relationship method name. The value of the array is the fully-qualified
|
||||
| class name of the relationship, e.g. `'relationName' => RelationShipClass::class`.
|
||||
|
|
||||
*/
|
||||
'additional_relation_types' => [],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Additional relation return types
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using custom relation types its possible for the class name to not contain
|
||||
| the proper return type of the relation. The key of the array is the relationship
|
||||
| method name. The value of the array is the return type of the relation ('many'
|
||||
| or 'morphTo').
|
||||
| e.g. `'relationName' => 'many'`.
|
||||
|
|
||||
*/
|
||||
'additional_relation_return_types' => [],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Enforce nullable Eloquent relationships on not null columns
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When set to true (default), this option enforces nullable Eloquent relationships.
|
||||
| However, in cases where the application logic ensures the presence of related
|
||||
| records it may be desirable to set this option to false to avoid unwanted null warnings.
|
||||
|
|
||||
| Default: true
|
||||
| A not null column with no foreign key constraint will have a "nullable" relationship.
|
||||
| * @property int $not_null_column_with_no_foreign_key_constraint
|
||||
| * @property-read BelongsToVariation|null $notNullColumnWithNoForeignKeyConstraint
|
||||
|
|
||||
| Option: false
|
||||
| A not null column with no foreign key constraint will have a "not nullable" relationship.
|
||||
| * @property int $not_null_column_with_no_foreign_key_constraint
|
||||
| * @property-read BelongsToVariation $notNullColumnWithNoForeignKeyConstraint
|
||||
|
|
||||
*/
|
||||
|
||||
'enforce_nullable_relationships' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Make soft deletable relations nullable
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When set to true (default), relationships to models using SoftDeletes trait
|
||||
| will be marked as nullable. This is because soft-deleted records are excluded
|
||||
| from queries by default, meaning even non-nullable foreign keys can return
|
||||
| null when the related model is soft-deleted.
|
||||
|
|
||||
| Default: true
|
||||
| A relationship to a soft-deletable model will include |null in the type:
|
||||
| * @property-read Team|null $team
|
||||
|
|
||||
| Option: false
|
||||
| A relationship to a soft-deletable model will NOT include |null (unless
|
||||
| nullable for other reasons such as nullable foreign key column):
|
||||
| * @property-read Team $team
|
||||
|
|
||||
*/
|
||||
|
||||
'soft_deletes_force_nullable' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Run artisan commands after migrations to generate model helpers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The specified commands should run after migrations are finished running.
|
||||
|
|
||||
*/
|
||||
'post_migrate' => [
|
||||
// 'ide-helper:models --nowrite',
|
||||
],
|
||||
|
||||
];
|
||||
62
config/inertia.php
Normal file
62
config/inertia.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Server Side Rendering
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* These options configures if and how Inertia uses Server Side Rendering
|
||||
* to pre-render the initial visits made to your application's pages.
|
||||
*
|
||||
* You can specify a custom SSR bundle path, or omit it to let Inertia
|
||||
* try and automatically detect it for you.
|
||||
*
|
||||
* Do note that enabling these options will NOT automatically make SSR work,
|
||||
* as a separate rendering service needs to be available. To learn more,
|
||||
* please visit https://inertiajs.com/server-side-rendering
|
||||
*/
|
||||
'ssr' => [
|
||||
|
||||
'enabled' => false,
|
||||
|
||||
'url' => env('APP_URL', 'https://localhost'),
|
||||
|
||||
],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Testing
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* The values described here are used to locate Inertia components on the
|
||||
* filesystem. For instance, when using `assertInertia`, the assertion
|
||||
* attempts to locate the component as a file relative to any of the
|
||||
* paths AND with any of the extensions specified here.
|
||||
*/
|
||||
'testing' => [
|
||||
|
||||
'ensure_pages_exist' => true,
|
||||
|
||||
'page_paths' => [
|
||||
resource_path('ts/front/Pages'),
|
||||
],
|
||||
|
||||
'page_extensions' => [
|
||||
'js',
|
||||
'jsx',
|
||||
'svelte',
|
||||
'ts',
|
||||
'tsx',
|
||||
'vue',
|
||||
],
|
||||
],
|
||||
|
||||
'history' => [
|
||||
'encrypt' => false,
|
||||
],
|
||||
|
||||
];
|
||||
356
config/laravellocalization.php
Normal file
356
config/laravellocalization.php
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
// Uncomment the languages that your site supports - or add new ones.
|
||||
// These are sorted by the native name, which is the order you might show them in a language selector.
|
||||
// Regional languages are sorted by their base language, so "British English" sorts as "English, British".
|
||||
// Original config here: https://github.com/mcamara/laravel-localization/blob/master/src/config/config.php.
|
||||
// @phpcs:disable Squiz.Commenting.InlineComment.NoSpaceBefore, Generic.Files.LineLength.TooLong, Squiz.PHP.CommentedOutCode.Found
|
||||
'supportedLocales' => [
|
||||
// 'ace' => ['name' => 'Achinese', 'script' => 'Latn', 'native' => 'Aceh', 'regional' => ''],
|
||||
// 'af' => ['name' => 'Afrikaans', 'script' => 'Latn', 'native' => 'Afrikaans', 'regional' => 'af_ZA'],
|
||||
// 'agq' => ['name' => 'Aghem', 'script' => 'Latn', 'native' => 'Aghem', 'regional' => ''],
|
||||
// 'ak' => ['name' => 'Akan', 'script' => 'Latn', 'native' => 'Akan', 'regional' => 'ak_GH'],
|
||||
// 'an' => ['name' => 'Aragonese', 'script' => 'Latn', 'native' => 'aragonés', 'regional' => 'an_ES'],
|
||||
// 'cch' => ['name' => 'Atsam', 'script' => 'Latn', 'native' => 'Atsam', 'regional' => ''],
|
||||
// 'gn' => ['name' => 'Guaraní', 'script' => 'Latn', 'native' => 'Avañe’ẽ', 'regional' => ''],
|
||||
// 'ae' => ['name' => 'Avestan', 'script' => 'Latn', 'native' => 'avesta', 'regional' => ''],
|
||||
// 'ay' => ['name' => 'Aymara', 'script' => 'Latn', 'native' => 'aymar aru', 'regional' => 'ay_PE'],
|
||||
// 'az' => ['name' => 'Azerbaijani (Latin)', 'script' => 'Latn', 'native' => 'azərbaycanca', 'regional' => 'az_AZ'],
|
||||
// 'id' => ['name' => 'Indonesian', 'script' => 'Latn', 'native' => 'Bahasa Indonesia', 'regional' => 'id_ID'],
|
||||
// 'ms' => ['name' => 'Malay', 'script' => 'Latn', 'native' => 'Bahasa Melayu', 'regional' => 'ms_MY'],
|
||||
// 'bm' => ['name' => 'Bambara', 'script' => 'Latn', 'native' => 'bamanakan', 'regional' => ''],
|
||||
// 'jv' => ['name' => 'Javanese (Latin)', 'script' => 'Latn', 'native' => 'Basa Jawa', 'regional' => ''],
|
||||
// 'su' => ['name' => 'Sundanese', 'script' => 'Latn', 'native' => 'Basa Sunda', 'regional' => ''],
|
||||
// 'bh' => ['name' => 'Bihari', 'script' => 'Latn', 'native' => 'Bihari', 'regional' => ''],
|
||||
// 'bi' => ['name' => 'Bislama', 'script' => 'Latn', 'native' => 'Bislama', 'regional' => ''],
|
||||
// 'nb' => ['name' => 'Norwegian Bokmål', 'script' => 'Latn', 'native' => 'Bokmål', 'regional' => 'nb_NO'],
|
||||
// 'bs' => ['name' => 'Bosnian', 'script' => 'Latn', 'native' => 'bosanski', 'regional' => 'bs_BA'],
|
||||
// 'br' => ['name' => 'Breton', 'script' => 'Latn', 'native' => 'brezhoneg', 'regional' => 'br_FR'],
|
||||
// 'ca' => ['name' => 'Catalan', 'script' => 'Latn', 'native' => 'català', 'regional' => 'ca_ES'],
|
||||
// 'ch' => ['name' => 'Chamorro', 'script' => 'Latn', 'native' => 'Chamoru', 'regional' => ''],
|
||||
// 'ny' => ['name' => 'Chewa', 'script' => 'Latn', 'native' => 'chiCheŵa', 'regional' => ''],
|
||||
// 'kde' => ['name' => 'Makonde', 'script' => 'Latn', 'native' => 'Chimakonde', 'regional' => ''],
|
||||
// 'sn' => ['name' => 'Shona', 'script' => 'Latn', 'native' => 'chiShona', 'regional' => ''],
|
||||
// 'co' => ['name' => 'Corsican', 'script' => 'Latn', 'native' => 'corsu', 'regional' => ''],
|
||||
// 'cy' => ['name' => 'Welsh', 'script' => 'Latn', 'native' => 'Cymraeg', 'regional' => 'cy_GB'],
|
||||
// 'da' => ['name' => 'Danish', 'script' => 'Latn', 'native' => 'dansk', 'regional' => 'da_DK'],
|
||||
// 'se' => ['name' => 'Northern Sami', 'script' => 'Latn', 'native' => 'davvisámegiella', 'regional' => 'se_NO'],
|
||||
// 'de' => ['name' => 'German', 'script' => 'Latn', 'native' => 'Deutsch', 'regional' => 'de_DE'],
|
||||
// 'luo' => ['name' => 'Luo', 'script' => 'Latn', 'native' => 'Dholuo', 'regional' => ''],
|
||||
// 'nv' => ['name' => 'Navajo', 'script' => 'Latn', 'native' => 'Diné bizaad', 'regional' => ''],
|
||||
// 'dua' => ['name' => 'Duala', 'script' => 'Latn', 'native' => 'duálá', 'regional' => ''],
|
||||
// 'et' => ['name' => 'Estonian', 'script' => 'Latn', 'native' => 'eesti', 'regional' => 'et_EE'],
|
||||
// 'na' => ['name' => 'Nauru', 'script' => 'Latn', 'native' => 'Ekakairũ Naoero', 'regional' => ''],
|
||||
// 'guz' => ['name' => 'Ekegusii', 'script' => 'Latn', 'native' => 'Ekegusii', 'regional' => ''],
|
||||
'en' => ['name' => 'English', 'script' => 'Latn', 'native' => 'English', 'regional' => 'en_GB'],
|
||||
// 'en-AU' => ['name' => 'Australian English', 'script' => 'Latn', 'native' => 'Australian English', 'regional' => 'en_AU'],
|
||||
// 'en-GB' => ['name' => 'British English', 'script' => 'Latn', 'native' => 'British English', 'regional' => 'en_GB'],
|
||||
// 'en-CA' => ['name' => 'Canadian English', 'script' => 'Latn', 'native' => 'Canadian English', 'regional' => 'en_CA'],
|
||||
// 'en-US' => ['name' => 'U.S. English', 'script' => 'Latn', 'native' => 'U.S. English', 'regional' => 'en_US'],
|
||||
// 'es' => ['name' => 'Spanish', 'script' => 'Latn', 'native' => 'español', 'regional' => 'es_ES'],
|
||||
// 'eo' => ['name' => 'Esperanto', 'script' => 'Latn', 'native' => 'esperanto', 'regional' => ''],
|
||||
// 'eu' => ['name' => 'Basque', 'script' => 'Latn', 'native' => 'euskara', 'regional' => 'eu_ES'],
|
||||
// 'ewo' => ['name' => 'Ewondo', 'script' => 'Latn', 'native' => 'ewondo', 'regional' => ''],
|
||||
// 'ee' => ['name' => 'Ewe', 'script' => 'Latn', 'native' => 'eʋegbe', 'regional' => ''],
|
||||
// 'fil' => ['name' => 'Filipino', 'script' => 'Latn', 'native' => 'Filipino', 'regional' => 'fil_PH'],
|
||||
'fr' => ['name' => 'French', 'script' => 'Latn', 'native' => 'français', 'regional' => 'fr_FR'],
|
||||
// 'fr-CA' => ['name' => 'Canadian French', 'script' => 'Latn', 'native' => 'français canadien', 'regional' => 'fr_CA'],
|
||||
// 'fy' => ['name' => 'Western Frisian', 'script' => 'Latn', 'native' => 'frysk', 'regional' => 'fy_DE'],
|
||||
// 'fur' => ['name' => 'Friulian', 'script' => 'Latn', 'native' => 'furlan', 'regional' => 'fur_IT'],
|
||||
// 'fo' => ['name' => 'Faroese', 'script' => 'Latn', 'native' => 'føroyskt', 'regional' => 'fo_FO'],
|
||||
// 'gaa' => ['name' => 'Ga', 'script' => 'Latn', 'native' => 'Ga', 'regional' => ''],
|
||||
// 'ga' => ['name' => 'Irish', 'script' => 'Latn', 'native' => 'Gaeilge', 'regional' => 'ga_IE'],
|
||||
// 'gv' => ['name' => 'Manx', 'script' => 'Latn', 'native' => 'Gaelg', 'regional' => 'gv_GB'],
|
||||
// 'sm' => ['name' => 'Samoan', 'script' => 'Latn', 'native' => 'Gagana fa’a Sāmoa', 'regional' => ''],
|
||||
// 'gl' => ['name' => 'Galician', 'script' => 'Latn', 'native' => 'galego', 'regional' => 'gl_ES'],
|
||||
// 'ki' => ['name' => 'Kikuyu', 'script' => 'Latn', 'native' => 'Gikuyu', 'regional' => ''],
|
||||
// 'gd' => ['name' => 'Scottish Gaelic', 'script' => 'Latn', 'native' => 'Gàidhlig', 'regional' => 'gd_GB'],
|
||||
// 'ha' => ['name' => 'Hausa', 'script' => 'Latn', 'native' => 'Hausa', 'regional' => 'ha_NG'],
|
||||
// 'bez' => ['name' => 'Bena', 'script' => 'Latn', 'native' => 'Hibena', 'regional' => ''],
|
||||
// 'ho' => ['name' => 'Hiri Motu', 'script' => 'Latn', 'native' => 'Hiri Motu', 'regional' => ''],
|
||||
// 'hr' => ['name' => 'Croatian', 'script' => 'Latn', 'native' => 'hrvatski', 'regional' => 'hr_HR'],
|
||||
// 'bem' => ['name' => 'Bemba', 'script' => 'Latn', 'native' => 'Ichibemba', 'regional' => 'bem_ZM'],
|
||||
// 'io' => ['name' => 'Ido', 'script' => 'Latn', 'native' => 'Ido', 'regional' => ''],
|
||||
// 'ig' => ['name' => 'Igbo', 'script' => 'Latn', 'native' => 'Igbo', 'regional' => 'ig_NG'],
|
||||
// 'rn' => ['name' => 'Rundi', 'script' => 'Latn', 'native' => 'Ikirundi', 'regional' => ''],
|
||||
// 'ia' => ['name' => 'Interlingua', 'script' => 'Latn', 'native' => 'interlingua', 'regional' => 'ia_FR'],
|
||||
// 'iu-Latn' => ['name' => 'Inuktitut (Latin)', 'script' => 'Latn', 'native' => 'Inuktitut', 'regional' => 'iu_CA'],
|
||||
// 'sbp' => ['name' => 'Sileibi', 'script' => 'Latn', 'native' => 'Ishisangu', 'regional' => ''],
|
||||
// 'nd' => ['name' => 'North Ndebele', 'script' => 'Latn', 'native' => 'isiNdebele', 'regional' => ''],
|
||||
// 'nr' => ['name' => 'South Ndebele', 'script' => 'Latn', 'native' => 'isiNdebele', 'regional' => 'nr_ZA'],
|
||||
// 'xh' => ['name' => 'Xhosa', 'script' => 'Latn', 'native' => 'isiXhosa', 'regional' => 'xh_ZA'],
|
||||
// 'zu' => ['name' => 'Zulu', 'script' => 'Latn', 'native' => 'isiZulu', 'regional' => 'zu_ZA'],
|
||||
// 'it' => ['name' => 'Italian', 'script' => 'Latn', 'native' => 'italiano', 'regional' => 'it_IT'],
|
||||
// 'ik' => ['name' => 'Inupiaq', 'script' => 'Latn', 'native' => 'Iñupiaq', 'regional' => 'ik_CA'],
|
||||
// 'dyo' => ['name' => 'Jola-Fonyi', 'script' => 'Latn', 'native' => 'joola', 'regional' => ''],
|
||||
// 'kea' => ['name' => 'Kabuverdianu', 'script' => 'Latn', 'native' => 'kabuverdianu', 'regional' => ''],
|
||||
// 'kaj' => ['name' => 'Jju', 'script' => 'Latn', 'native' => 'Kaje', 'regional' => ''],
|
||||
// 'mh' => ['name' => 'Marshallese', 'script' => 'Latn', 'native' => 'Kajin M̧ajeļ', 'regional' => 'mh_MH'],
|
||||
// 'kl' => ['name' => 'Kalaallisut', 'script' => 'Latn', 'native' => 'kalaallisut', 'regional' => 'kl_GL'],
|
||||
// 'kln' => ['name' => 'Kalenjin', 'script' => 'Latn', 'native' => 'Kalenjin', 'regional' => ''],
|
||||
// 'kr' => ['name' => 'Kanuri', 'script' => 'Latn', 'native' => 'Kanuri', 'regional' => ''],
|
||||
// 'kcg' => ['name' => 'Tyap', 'script' => 'Latn', 'native' => 'Katab', 'regional' => ''],
|
||||
// 'kw' => ['name' => 'Cornish', 'script' => 'Latn', 'native' => 'kernewek', 'regional' => 'kw_GB'],
|
||||
// 'naq' => ['name' => 'Nama', 'script' => 'Latn', 'native' => 'Khoekhoegowab', 'regional' => ''],
|
||||
// 'rof' => ['name' => 'Rombo', 'script' => 'Latn', 'native' => 'Kihorombo', 'regional' => ''],
|
||||
// 'kam' => ['name' => 'Kamba', 'script' => 'Latn', 'native' => 'Kikamba', 'regional' => ''],
|
||||
// 'kg' => ['name' => 'Kongo', 'script' => 'Latn', 'native' => 'Kikongo', 'regional' => ''],
|
||||
// 'jmc' => ['name' => 'Machame', 'script' => 'Latn', 'native' => 'Kimachame', 'regional' => ''],
|
||||
// 'rw' => ['name' => 'Kinyarwanda', 'script' => 'Latn', 'native' => 'Kinyarwanda', 'regional' => 'rw_RW'],
|
||||
// 'asa' => ['name' => 'Kipare', 'script' => 'Latn', 'native' => 'Kipare', 'regional' => ''],
|
||||
// 'rwk' => ['name' => 'Rwa', 'script' => 'Latn', 'native' => 'Kiruwa', 'regional' => ''],
|
||||
// 'saq' => ['name' => 'Samburu', 'script' => 'Latn', 'native' => 'Kisampur', 'regional' => ''],
|
||||
// 'ksb' => ['name' => 'Shambala', 'script' => 'Latn', 'native' => 'Kishambaa', 'regional' => ''],
|
||||
// 'swc' => ['name' => 'Congo Swahili', 'script' => 'Latn', 'native' => 'Kiswahili ya Kongo', 'regional' => ''],
|
||||
// 'sw' => ['name' => 'Swahili', 'script' => 'Latn', 'native' => 'Kiswahili', 'regional' => 'sw_KE'],
|
||||
// 'dav' => ['name' => 'Dawida', 'script' => 'Latn', 'native' => 'Kitaita', 'regional' => ''],
|
||||
// 'teo' => ['name' => 'Teso', 'script' => 'Latn', 'native' => 'Kiteso', 'regional' => ''],
|
||||
// 'khq' => ['name' => 'Koyra Chiini', 'script' => 'Latn', 'native' => 'Koyra ciini', 'regional' => ''],
|
||||
// 'ses' => ['name' => 'Songhay', 'script' => 'Latn', 'native' => 'Koyraboro senni', 'regional' => ''],
|
||||
// 'mfe' => ['name' => 'Morisyen', 'script' => 'Latn', 'native' => 'kreol morisien', 'regional' => ''],
|
||||
// 'ht' => ['name' => 'Haitian', 'script' => 'Latn', 'native' => 'Kreyòl ayisyen', 'regional' => 'ht_HT'],
|
||||
// 'kj' => ['name' => 'Kuanyama', 'script' => 'Latn', 'native' => 'Kwanyama', 'regional' => ''],
|
||||
// 'ksh' => ['name' => 'Kölsch', 'script' => 'Latn', 'native' => 'Kölsch', 'regional' => ''],
|
||||
// 'ebu' => ['name' => 'Kiembu', 'script' => 'Latn', 'native' => 'Kĩembu', 'regional' => ''],
|
||||
// 'mer' => ['name' => 'Kimîîru', 'script' => 'Latn', 'native' => 'Kĩmĩrũ', 'regional' => ''],
|
||||
// 'lag' => ['name' => 'Langi', 'script' => 'Latn', 'native' => 'Kɨlaangi', 'regional' => ''],
|
||||
// 'lah' => ['name' => 'Lahnda', 'script' => 'Latn', 'native' => 'Lahnda', 'regional' => ''],
|
||||
// 'la' => ['name' => 'Latin', 'script' => 'Latn', 'native' => 'latine', 'regional' => ''],
|
||||
// 'lv' => ['name' => 'Latvian', 'script' => 'Latn', 'native' => 'latviešu', 'regional' => 'lv_LV'],
|
||||
// 'to' => ['name' => 'Tongan', 'script' => 'Latn', 'native' => 'lea fakatonga', 'regional' => ''],
|
||||
// 'lt' => ['name' => 'Lithuanian', 'script' => 'Latn', 'native' => 'lietuvių', 'regional' => 'lt_LT'],
|
||||
// 'li' => ['name' => 'Limburgish', 'script' => 'Latn', 'native' => 'Limburgs', 'regional' => 'li_BE'],
|
||||
// 'ln' => ['name' => 'Lingala', 'script' => 'Latn', 'native' => 'lingála', 'regional' => ''],
|
||||
// 'lg' => ['name' => 'Ganda', 'script' => 'Latn', 'native' => 'Luganda', 'regional' => 'lg_UG'],
|
||||
// 'luy' => ['name' => 'Oluluyia', 'script' => 'Latn', 'native' => 'Luluhia', 'regional' => ''],
|
||||
// 'lb' => ['name' => 'Luxembourgish', 'script' => 'Latn', 'native' => 'Lëtzebuergesch', 'regional' => 'lb_LU'],
|
||||
// 'hu' => ['name' => 'Hungarian', 'script' => 'Latn', 'native' => 'magyar', 'regional' => 'hu_HU'],
|
||||
// 'mgh' => ['name' => 'Makhuwa-Meetto', 'script' => 'Latn', 'native' => 'Makua', 'regional' => ''],
|
||||
// 'mg' => ['name' => 'Malagasy', 'script' => 'Latn', 'native' => 'Malagasy', 'regional' => 'mg_MG'],
|
||||
// 'mt' => ['name' => 'Maltese', 'script' => 'Latn', 'native' => 'Malti', 'regional' => 'mt_MT'],
|
||||
// 'mtr' => ['name' => 'Mewari', 'script' => 'Latn', 'native' => 'Mewari', 'regional' => ''],
|
||||
// 'mua' => ['name' => 'Mundang', 'script' => 'Latn', 'native' => 'Mundang', 'regional' => ''],
|
||||
// 'mi' => ['name' => 'Māori', 'script' => 'Latn', 'native' => 'Māori', 'regional' => 'mi_NZ'],
|
||||
// 'nl' => ['name' => 'Dutch', 'script' => 'Latn', 'native' => 'Nederlands', 'regional' => 'nl_NL'],
|
||||
// 'nmg' => ['name' => 'Kwasio', 'script' => 'Latn', 'native' => 'ngumba', 'regional' => ''],
|
||||
// 'yav' => ['name' => 'Yangben', 'script' => 'Latn', 'native' => 'nuasue', 'regional' => ''],
|
||||
// 'nn' => ['name' => 'Norwegian Nynorsk', 'script' => 'Latn', 'native' => 'nynorsk', 'regional' => 'nn_NO'],
|
||||
// 'oc' => ['name' => 'Occitan', 'script' => 'Latn', 'native' => 'occitan', 'regional' => 'oc_FR'],
|
||||
// 'ang' => ['name' => 'Old English', 'script' => 'Runr', 'native' => 'Old English', 'regional' => ''],
|
||||
// 'xog' => ['name' => 'Soga', 'script' => 'Latn', 'native' => 'Olusoga', 'regional' => ''],
|
||||
// 'om' => ['name' => 'Oromo', 'script' => 'Latn', 'native' => 'Oromoo', 'regional' => 'om_ET'],
|
||||
// 'ng' => ['name' => 'Ndonga', 'script' => 'Latn', 'native' => 'OshiNdonga', 'regional' => ''],
|
||||
// 'hz' => ['name' => 'Herero', 'script' => 'Latn', 'native' => 'Otjiherero', 'regional' => ''],
|
||||
// 'uz-Latn' => ['name' => 'Uzbek (Latin)', 'script' => 'Latn', 'native' => 'oʼzbekcha', 'regional' => 'uz_UZ'],
|
||||
// 'nds' => ['name' => 'Low German', 'script' => 'Latn', 'native' => 'Plattdüütsch', 'regional' => 'nds_DE'],
|
||||
// 'pl' => ['name' => 'Polish', 'script' => 'Latn', 'native' => 'polski', 'regional' => 'pl_PL'],
|
||||
// 'pt' => ['name' => 'Portuguese', 'script' => 'Latn', 'native' => 'português', 'regional' => 'pt_PT'],
|
||||
// 'pt-BR' => ['name' => 'Brazilian Portuguese', 'script' => 'Latn', 'native' => 'português do Brasil', 'regional' => 'pt_BR'],
|
||||
// 'ff' => ['name' => 'Fulah', 'script' => 'Latn', 'native' => 'Pulaar', 'regional' => 'ff_SN'],
|
||||
// 'pi' => ['name' => 'Pahari-Potwari', 'script' => 'Latn', 'native' => 'Pāli', 'regional' => ''],
|
||||
// 'aa' => ['name' => 'Afar', 'script' => 'Latn', 'native' => 'Qafar', 'regional' => 'aa_ER'],
|
||||
// 'ty' => ['name' => 'Tahitian', 'script' => 'Latn', 'native' => 'Reo Māohi', 'regional' => ''],
|
||||
// 'ksf' => ['name' => 'Bafia', 'script' => 'Latn', 'native' => 'rikpa', 'regional' => ''],
|
||||
// 'ro' => ['name' => 'Romanian', 'script' => 'Latn', 'native' => 'română', 'regional' => 'ro_RO'],
|
||||
// 'cgg' => ['name' => 'Chiga', 'script' => 'Latn', 'native' => 'Rukiga', 'regional' => ''],
|
||||
// 'rm' => ['name' => 'Romansh', 'script' => 'Latn', 'native' => 'rumantsch', 'regional' => ''],
|
||||
// 'qu' => ['name' => 'Quechua', 'script' => 'Latn', 'native' => 'Runa Simi', 'regional' => ''],
|
||||
// 'nyn' => ['name' => 'Nyankole', 'script' => 'Latn', 'native' => 'Runyankore', 'regional' => ''],
|
||||
// 'ssy' => ['name' => 'Saho', 'script' => 'Latn', 'native' => 'Saho', 'regional' => ''],
|
||||
// 'sc' => ['name' => 'Sardinian', 'script' => 'Latn', 'native' => 'sardu', 'regional' => 'sc_IT'],
|
||||
// 'de-CH' => ['name' => 'Swiss High German', 'script' => 'Latn', 'native' => 'Schweizer Hochdeutsch', 'regional' => 'de_CH'],
|
||||
// 'gsw' => ['name' => 'Swiss German', 'script' => 'Latn', 'native' => 'Schwiizertüütsch', 'regional' => ''],
|
||||
// 'trv' => ['name' => 'Taroko', 'script' => 'Latn', 'native' => 'Seediq', 'regional' => ''],
|
||||
// 'seh' => ['name' => 'Sena', 'script' => 'Latn', 'native' => 'sena', 'regional' => ''],
|
||||
// 'nso' => ['name' => 'Northern Sotho', 'script' => 'Latn', 'native' => 'Sesotho sa Leboa', 'regional' => 'nso_ZA'],
|
||||
// 'st' => ['name' => 'Southern Sotho', 'script' => 'Latn', 'native' => 'Sesotho', 'regional' => 'st_ZA'],
|
||||
// 'tn' => ['name' => 'Tswana', 'script' => 'Latn', 'native' => 'Setswana', 'regional' => 'tn_ZA'],
|
||||
// 'sq' => ['name' => 'Albanian', 'script' => 'Latn', 'native' => 'shqip', 'regional' => 'sq_AL'],
|
||||
// 'sid' => ['name' => 'Sidamo', 'script' => 'Latn', 'native' => 'Sidaamu Afo', 'regional' => 'sid_ET'],
|
||||
// 'ss' => ['name' => 'Swati', 'script' => 'Latn', 'native' => 'Siswati', 'regional' => 'ss_ZA'],
|
||||
// 'sk' => ['name' => 'Slovak', 'script' => 'Latn', 'native' => 'slovenčina', 'regional' => 'sk_SK'],
|
||||
// 'sl' => ['name' => 'Slovene', 'script' => 'Latn', 'native' => 'slovenščina', 'regional' => 'sl_SI'],
|
||||
// 'so' => ['name' => 'Somali', 'script' => 'Latn', 'native' => 'Soomaali', 'regional' => 'so_SO'],
|
||||
// 'sr-Latn' => ['name' => 'Serbian (Latin)', 'script' => 'Latn', 'native' => 'Srpski', 'regional' => 'sr_RS'],
|
||||
// 'sh' => ['name' => 'Serbo-Croatian', 'script' => 'Latn', 'native' => 'srpskohrvatski', 'regional' => ''],
|
||||
// 'fi' => ['name' => 'Finnish', 'script' => 'Latn', 'native' => 'suomi', 'regional' => 'fi_FI'],
|
||||
// 'sv' => ['name' => 'Swedish', 'script' => 'Latn', 'native' => 'svenska', 'regional' => 'sv_SE'],
|
||||
// 'sg' => ['name' => 'Sango', 'script' => 'Latn', 'native' => 'Sängö', 'regional' => ''],
|
||||
// 'tl' => ['name' => 'Tagalog', 'script' => 'Latn', 'native' => 'Tagalog', 'regional' => 'tl_PH'],
|
||||
// 'tzm-Latn' => ['name' => 'Central Atlas Tamazight (Latin)', 'script' => 'Latn', 'native' => 'Tamazight', 'regional' => ''],
|
||||
// 'kab' => ['name' => 'Kabyle', 'script' => 'Latn', 'native' => 'Taqbaylit', 'regional' => 'kab_DZ'],
|
||||
// 'twq' => ['name' => 'Tasawaq', 'script' => 'Latn', 'native' => 'Tasawaq senni', 'regional' => ''],
|
||||
// 'shi' => ['name' => 'Tachelhit (Latin)', 'script' => 'Latn', 'native' => 'Tashelhit', 'regional' => ''],
|
||||
// 'nus' => ['name' => 'Nuer', 'script' => 'Latn', 'native' => 'Thok Nath', 'regional' => ''],
|
||||
// 'vi' => ['name' => 'Vietnamese', 'script' => 'Latn', 'native' => 'Tiếng Việt', 'regional' => 'vi_VN'],
|
||||
// 'tg-Latn' => ['name' => 'Tajik (Latin)', 'script' => 'Latn', 'native' => 'tojikī', 'regional' => 'tg_TJ'],
|
||||
// 'lu' => ['name' => 'Luba-Katanga', 'script' => 'Latn', 'native' => 'Tshiluba', 'regional' => 've_ZA'],
|
||||
// 've' => ['name' => 'Venda', 'script' => 'Latn', 'native' => 'Tshivenḓa', 'regional' => ''],
|
||||
// 'tw' => ['name' => 'Twi', 'script' => 'Latn', 'native' => 'Twi', 'regional' => ''],
|
||||
// 'tr' => ['name' => 'Turkish', 'script' => 'Latn', 'native' => 'Türkçe', 'regional' => 'tr_TR'],
|
||||
// 'ale' => ['name' => 'Aleut', 'script' => 'Latn', 'native' => 'Unangax tunuu', 'regional' => ''],
|
||||
// 'ca-valencia' => ['name' => 'Valencian', 'script' => 'Latn', 'native' => 'valencià', 'regional' => ''],
|
||||
// 'vai-Latn' => ['name' => 'Vai (Latin)', 'script' => 'Latn', 'native' => 'Viyamíĩ', 'regional' => ''],
|
||||
// 'vo' => ['name' => 'Volapük', 'script' => 'Latn', 'native' => 'Volapük', 'regional' => ''],
|
||||
// 'fj' => ['name' => 'Fijian', 'script' => 'Latn', 'native' => 'vosa Vakaviti', 'regional' => ''],
|
||||
// 'wa' => ['name' => 'Walloon', 'script' => 'Latn', 'native' => 'Walon', 'regional' => 'wa_BE'],
|
||||
// 'wae' => ['name' => 'Walser', 'script' => 'Latn', 'native' => 'Walser', 'regional' => 'wae_CH'],
|
||||
// 'wen' => ['name' => 'Sorbian', 'script' => 'Latn', 'native' => 'Wendic', 'regional' => ''],
|
||||
// 'wo' => ['name' => 'Wolof', 'script' => 'Latn', 'native' => 'Wolof', 'regional' => 'wo_SN'],
|
||||
// 'ts' => ['name' => 'Tsonga', 'script' => 'Latn', 'native' => 'Xitsonga', 'regional' => 'ts_ZA'],
|
||||
// 'dje' => ['name' => 'Zarma', 'script' => 'Latn', 'native' => 'Zarmaciine', 'regional' => ''],
|
||||
// 'yo' => ['name' => 'Yoruba', 'script' => 'Latn', 'native' => 'Èdè Yorùbá', 'regional' => 'yo_NG'],
|
||||
// 'de-AT' => ['name' => 'Austrian German', 'script' => 'Latn', 'native' => 'Österreichisches Deutsch', 'regional' => 'de_AT'],
|
||||
// 'is' => ['name' => 'Icelandic', 'script' => 'Latn', 'native' => 'íslenska', 'regional' => 'is_IS'],
|
||||
// 'cs' => ['name' => 'Czech', 'script' => 'Latn', 'native' => 'čeština', 'regional' => 'cs_CZ'],
|
||||
// 'bas' => ['name' => 'Basa', 'script' => 'Latn', 'native' => 'Ɓàsàa', 'regional' => ''],
|
||||
// 'mas' => ['name' => 'Masai', 'script' => 'Latn', 'native' => 'ɔl-Maa', 'regional' => ''],
|
||||
// 'haw' => ['name' => 'Hawaiian', 'script' => 'Latn', 'native' => 'ʻŌlelo Hawaiʻi', 'regional' => ''],
|
||||
// 'el' => ['name' => 'Greek', 'script' => 'Grek', 'native' => 'Ελληνικά', 'regional' => 'el_GR'],
|
||||
// 'uz' => ['name' => 'Uzbek (Cyrillic)', 'script' => 'Cyrl', 'native' => 'Ўзбек', 'regional' => 'uz_UZ'],
|
||||
// 'az-Cyrl' => ['name' => 'Azerbaijani (Cyrillic)', 'script' => 'Cyrl', 'native' => 'Азәрбајҹан', 'regional' => 'uz_UZ'],
|
||||
// 'ab' => ['name' => 'Abkhazian', 'script' => 'Cyrl', 'native' => 'Аҧсуа', 'regional' => ''],
|
||||
// 'os' => ['name' => 'Ossetic', 'script' => 'Cyrl', 'native' => 'Ирон', 'regional' => 'os_RU'],
|
||||
// 'ky' => ['name' => 'Kyrgyz', 'script' => 'Cyrl', 'native' => 'Кыргыз', 'regional' => 'ky_KG'],
|
||||
// 'sr' => ['name' => 'Serbian (Cyrillic)', 'script' => 'Cyrl', 'native' => 'Српски', 'regional' => 'sr_RS'],
|
||||
// 'av' => ['name' => 'Avaric', 'script' => 'Cyrl', 'native' => 'авар мацӀ', 'regional' => ''],
|
||||
// 'ady' => ['name' => 'Adyghe', 'script' => 'Cyrl', 'native' => 'адыгэбзэ', 'regional' => ''],
|
||||
// 'ba' => ['name' => 'Bashkir', 'script' => 'Cyrl', 'native' => 'башҡорт теле', 'regional' => ''],
|
||||
// 'be' => ['name' => 'Belarusian', 'script' => 'Cyrl', 'native' => 'беларуская', 'regional' => 'be_BY'],
|
||||
// 'bg' => ['name' => 'Bulgarian', 'script' => 'Cyrl', 'native' => 'български', 'regional' => 'bg_BG'],
|
||||
// 'kv' => ['name' => 'Komi', 'script' => 'Cyrl', 'native' => 'коми кыв', 'regional' => ''],
|
||||
// 'mk' => ['name' => 'Macedonian', 'script' => 'Cyrl', 'native' => 'македонски', 'regional' => 'mk_MK'],
|
||||
// 'mn' => ['name' => 'Mongolian (Cyrillic)', 'script' => 'Cyrl', 'native' => 'монгол', 'regional' => 'mn_MN'],
|
||||
// 'ce' => ['name' => 'Chechen', 'script' => 'Cyrl', 'native' => 'нохчийн мотт', 'regional' => 'ce_RU'],
|
||||
// 'ru' => ['name' => 'Russian', 'script' => 'Cyrl', 'native' => 'русский', 'regional' => 'ru_RU'],
|
||||
// 'sah' => ['name' => 'Yakut', 'script' => 'Cyrl', 'native' => 'саха тыла', 'regional' => ''],
|
||||
// 'tt' => ['name' => 'Tatar', 'script' => 'Cyrl', 'native' => 'татар теле', 'regional' => 'tt_RU'],
|
||||
// 'tg' => ['name' => 'Tajik (Cyrillic)', 'script' => 'Cyrl', 'native' => 'тоҷикӣ', 'regional' => 'tg_TJ'],
|
||||
// 'tk' => ['name' => 'Turkmen', 'script' => 'Cyrl', 'native' => 'түркменче', 'regional' => 'tk_TM'],
|
||||
// 'uk' => ['name' => 'Ukrainian', 'script' => 'Cyrl', 'native' => 'українська', 'regional' => 'uk_UA'],
|
||||
// 'cv' => ['name' => 'Chuvash', 'script' => 'Cyrl', 'native' => 'чӑваш чӗлхи', 'regional' => 'cv_RU'],
|
||||
// 'cu' => ['name' => 'Church Slavic', 'script' => 'Cyrl', 'native' => 'ѩзыкъ словѣньскъ', 'regional' => ''],
|
||||
// 'kk' => ['name' => 'Kazakh', 'script' => 'Cyrl', 'native' => 'қазақ тілі', 'regional' => 'kk_KZ'],
|
||||
// 'hy' => ['name' => 'Armenian', 'script' => 'Armn', 'native' => 'Հայերեն', 'regional' => 'hy_AM'],
|
||||
// 'yi' => ['name' => 'Yiddish', 'script' => 'Hebr', 'native' => 'ייִדיש', 'regional' => 'yi_US'],
|
||||
// 'he' => ['name' => 'Hebrew', 'script' => 'Hebr', 'native' => 'עברית', 'regional' => 'he_IL'],
|
||||
// 'ug' => ['name' => 'Uyghur', 'script' => 'Arab', 'native' => 'ئۇيغۇرچە', 'regional' => 'ug_CN'],
|
||||
// 'ur' => ['name' => 'Urdu', 'script' => 'Arab', 'native' => 'اردو', 'regional' => 'ur_PK'],
|
||||
// 'ar' => ['name' => 'Arabic', 'script' => 'Arab', 'native' => 'العربية', 'regional' => 'ar_AE'],
|
||||
// 'uz-Arab' => ['name' => 'Uzbek (Arabic)', 'script' => 'Arab', 'native' => 'اۉزبېک', 'regional' => ''],
|
||||
// 'tg-Arab' => ['name' => 'Tajik (Arabic)', 'script' => 'Arab', 'native' => 'تاجیکی', 'regional' => 'tg_TJ'],
|
||||
// 'sd' => ['name' => 'Sindhi', 'script' => 'Arab', 'native' => 'سنڌي', 'regional' => 'sd_IN'],
|
||||
// 'fa' => ['name' => 'Persian', 'script' => 'Arab', 'native' => 'فارسی', 'regional' => 'fa_IR'],
|
||||
// 'pa-Arab' => ['name' => 'Punjabi (Arabic)', 'script' => 'Arab', 'native' => 'پنجاب', 'regional' => 'pa_IN'],
|
||||
// 'ps' => ['name' => 'Pashto', 'script' => 'Arab', 'native' => 'پښتو', 'regional' => 'ps_AF'],
|
||||
// 'ks' => ['name' => 'Kashmiri (Arabic)', 'script' => 'Arab', 'native' => 'کأشُر', 'regional' => 'ks_IN'],
|
||||
// 'ku' => ['name' => 'Kurdish', 'script' => 'Arab', 'native' => 'کوردی', 'regional' => 'ku_TR'],
|
||||
// 'dv' => ['name' => 'Divehi', 'script' => 'Thaa', 'native' => 'ދިވެހިބަސް', 'regional' => 'dv_MV'],
|
||||
// 'ks-Deva' => ['name' => 'Kashmiri (Devaganari)', 'script' => 'Deva', 'native' => 'कॉशुर', 'regional' => 'ks_IN'],
|
||||
// 'kok' => ['name' => 'Konkani', 'script' => 'Deva', 'native' => 'कोंकणी', 'regional' => 'kok_IN'],
|
||||
// 'doi' => ['name' => 'Dogri', 'script' => 'Deva', 'native' => 'डोगरी', 'regional' => 'doi_IN'],
|
||||
// 'ne' => ['name' => 'Nepali', 'script' => 'Deva', 'native' => 'नेपाली', 'regional' => ''],
|
||||
// 'pra' => ['name' => 'Prakrit', 'script' => 'Deva', 'native' => 'प्राकृत', 'regional' => ''],
|
||||
// 'brx' => ['name' => 'Bodo', 'script' => 'Deva', 'native' => 'बड़ो', 'regional' => 'brx_IN'],
|
||||
// 'bra' => ['name' => 'Braj', 'script' => 'Deva', 'native' => 'ब्रज भाषा', 'regional' => ''],
|
||||
// 'mr' => ['name' => 'Marathi', 'script' => 'Deva', 'native' => 'मराठी', 'regional' => 'mr_IN'],
|
||||
// 'mai' => ['name' => 'Maithili', 'script' => 'Tirh', 'native' => 'मैथिली', 'regional' => 'mai_IN'],
|
||||
// 'raj' => ['name' => 'Rajasthani', 'script' => 'Deva', 'native' => 'राजस्थानी', 'regional' => ''],
|
||||
// 'sa' => ['name' => 'Sanskrit', 'script' => 'Deva', 'native' => 'संस्कृतम्', 'regional' => 'sa_IN'],
|
||||
// 'hi' => ['name' => 'Hindi', 'script' => 'Deva', 'native' => 'हिन्दी', 'regional' => 'hi_IN'],
|
||||
// 'as' => ['name' => 'Assamese', 'script' => 'Beng', 'native' => 'অসমীয়া', 'regional' => 'as_IN'],
|
||||
// 'bn' => ['name' => 'Bengali', 'script' => 'Beng', 'native' => 'বাংলা', 'regional' => 'bn_BD'],
|
||||
// 'mni' => ['name' => 'Manipuri', 'script' => 'Beng', 'native' => 'মৈতৈ', 'regional' => 'mni_IN'],
|
||||
// 'pa' => ['name' => 'Punjabi (Gurmukhi)', 'script' => 'Guru', 'native' => 'ਪੰਜਾਬੀ', 'regional' => 'pa_IN'],
|
||||
// 'gu' => ['name' => 'Gujarati', 'script' => 'Gujr', 'native' => 'ગુજરાતી', 'regional' => 'gu_IN'],
|
||||
// 'or' => ['name' => 'Oriya', 'script' => 'Orya', 'native' => 'ଓଡ଼ିଆ', 'regional' => 'or_IN'],
|
||||
// 'ta' => ['name' => 'Tamil', 'script' => 'Taml', 'native' => 'தமிழ்', 'regional' => 'ta_IN'],
|
||||
// 'te' => ['name' => 'Telugu', 'script' => 'Telu', 'native' => 'తెలుగు', 'regional' => 'te_IN'],
|
||||
// 'kn' => ['name' => 'Kannada', 'script' => 'Knda', 'native' => 'ಕನ್ನಡ', 'regional' => 'kn_IN'],
|
||||
// 'ml' => ['name' => 'Malayalam', 'script' => 'Mlym', 'native' => 'മലയാളം', 'regional' => 'ml_IN'],
|
||||
// 'si' => ['name' => 'Sinhala', 'script' => 'Sinh', 'native' => 'සිංහල', 'regional' => 'si_LK'],
|
||||
// 'th' => ['name' => 'Thai', 'script' => 'Thai', 'native' => 'ไทย', 'regional' => 'th_TH'],
|
||||
// 'lo' => ['name' => 'Lao', 'script' => 'Laoo', 'native' => 'ລາວ', 'regional' => 'lo_LA'],
|
||||
// 'bo' => ['name' => 'Tibetan', 'script' => 'Tibt', 'native' => 'པོད་སྐད་', 'regional' => 'bo_IN'],
|
||||
// 'dz' => ['name' => 'Dzongkha', 'script' => 'Tibt', 'native' => 'རྫོང་ཁ', 'regional' => 'dz_BT'],
|
||||
// 'my' => ['name' => 'Burmese', 'script' => 'Mymr', 'native' => 'မြန်မာဘာသာ', 'regional' => 'my_MM'],
|
||||
// 'ka' => ['name' => 'Georgian', 'script' => 'Geor', 'native' => 'ქართული', 'regional' => 'ka_GE'],
|
||||
// 'byn' => ['name' => 'Blin', 'script' => 'Ethi', 'native' => 'ብሊን', 'regional' => 'byn_ER'],
|
||||
// 'tig' => ['name' => 'Tigre', 'script' => 'Ethi', 'native' => 'ትግረ', 'regional' => 'tig_ER'],
|
||||
// 'ti' => ['name' => 'Tigrinya', 'script' => 'Ethi', 'native' => 'ትግርኛ', 'regional' => 'ti_ET'],
|
||||
// 'am' => ['name' => 'Amharic', 'script' => 'Ethi', 'native' => 'አማርኛ', 'regional' => 'am_ET'],
|
||||
// 'wal' => ['name' => 'Wolaytta', 'script' => 'Ethi', 'native' => 'ወላይታቱ', 'regional' => 'wal_ET'],
|
||||
// 'chr' => ['name' => 'Cherokee', 'script' => 'Cher', 'native' => 'ᏣᎳᎩ', 'regional' => ''],
|
||||
// 'iu' => ['name' => 'Inuktitut (Canadian Aboriginal Syllabics)', 'script' => 'Cans', 'native' => 'ᐃᓄᒃᑎᑐᑦ', 'regional' => 'iu_CA'],
|
||||
// 'oj' => ['name' => 'Ojibwa', 'script' => 'Cans', 'native' => 'ᐊᓂᔑᓈᐯᒧᐎᓐ', 'regional' => ''],
|
||||
// 'cr' => ['name' => 'Cree', 'script' => 'Cans', 'native' => 'ᓀᐦᐃᔭᐍᐏᐣ', 'regional' => ''],
|
||||
// 'km' => ['name' => 'Khmer', 'script' => 'Khmr', 'native' => 'ភាសាខ្មែរ', 'regional' => 'km_KH'],
|
||||
// 'mn-Mong' => ['name' => 'Mongolian (Mongolian)', 'script' => 'Mong', 'native' => 'ᠮᠣᠨᠭᠭᠣᠯ ᠬᠡᠯᠡ', 'regional' => 'mn_MN'],
|
||||
// 'shi-Tfng' => ['name' => 'Tachelhit (Tifinagh)', 'script' => 'Tfng', 'native' => 'ⵜⴰⵎⴰⵣⵉⵖⵜ', 'regional' => ''],
|
||||
// 'tzm' => ['name' => 'Central Atlas Tamazight (Tifinagh)','script' => 'Tfng', 'native' => 'ⵜⴰⵎⴰⵣⵉⵖⵜ', 'regional' => ''],
|
||||
// 'yue' => ['name' => 'Yue', 'script' => 'Hant', 'native' => '廣州話', 'regional' => 'yue_HK'],
|
||||
// 'ja' => ['name' => 'Japanese', 'script' => 'Jpan', 'native' => '日本語', 'regional' => 'ja_JP'],
|
||||
// 'zh' => ['name' => 'Chinese (Simplified)', 'script' => 'Hans', 'native' => '简体中文', 'regional' => 'zh_CN'],
|
||||
// 'zh-Hant' => ['name' => 'Chinese (Traditional)', 'script' => 'Hant', 'native' => '繁體中文', 'regional' => 'zh_CN'],
|
||||
// 'ii' => ['name' => 'Sichuan Yi', 'script' => 'Yiii', 'native' => 'ꆈꌠꉙ', 'regional' => ''],
|
||||
// 'vai' => ['name' => 'Vai (Vai)', 'script' => 'Vaii', 'native' => 'ꕙꔤ', 'regional' => ''],
|
||||
// 'jv-Java' => ['name' => 'Javanese (Javanese)', 'script' => 'Java', 'native' => 'ꦧꦱꦗꦮ', 'regional' => ''],
|
||||
// 'ko' => ['name' => 'Korean', 'script' => 'Hang', 'native' => '한국어', 'regional' => 'ko_KR'],
|
||||
],
|
||||
// @phpcs:enable
|
||||
|
||||
// Requires middleware `LaravelSessionRedirect.php`.
|
||||
//
|
||||
// Automatically determine locale from browser
|
||||
// (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language)
|
||||
// on first call if it's not defined in the URL. Redirect user to computed localized url.
|
||||
// For example, if users browser language is `de`, and `de` is active in the array `supportedLocales`,
|
||||
// the `/about` would be redirected to `/de/about`.
|
||||
//
|
||||
// The locale will be stored in session and only be computed from browser
|
||||
// again if the session expires.
|
||||
//
|
||||
// If false, system will take app.php locale attribute.
|
||||
'useAcceptLanguageHeader' => false,
|
||||
|
||||
// If `hideDefaultLocaleInURL` is true, then a url without locale
|
||||
// is identical with the same url with default locale.
|
||||
// For example, if `en` is default locale, then `/en/about` and `/about`
|
||||
// would be identical.
|
||||
//
|
||||
// If in addition the middleware `LaravelLocalizationRedirectFilter` is active, then
|
||||
// every url with default locale is redirected to url without locale.
|
||||
// For example, `/en/about` would be redirected to `/about`.
|
||||
// It is recommended to use `hideDefaultLocaleInURL` only in
|
||||
// combination with the middleware `LaravelLocalizationRedirectFilter`
|
||||
// to avoid duplicate content (SEO).
|
||||
//
|
||||
// If `useAcceptLanguageHeader` is true, then the first time
|
||||
// the locale will be determined from browser and redirect to that language.
|
||||
// After that, `hideDefaultLocaleInURL` behaves as usual.
|
||||
'hideDefaultLocaleInURL' => true,
|
||||
|
||||
// If you want to display the locales in particular order in the language selector you should write the order here.
|
||||
// CAUTION: Please consider using the appropriate locale code otherwise it will not work
|
||||
// Example: 'localesOrder' => ['es','en'].
|
||||
'localesOrder' => ['fr', 'en'],
|
||||
|
||||
// If you want to use custom language URL segments like 'at' instead of 'de-AT', you can map them to allow the
|
||||
// LanguageNegotiator to assign the desired locales based on HTTP Accept Language Header. For example, if you want
|
||||
// to use 'at' instead of 'de-AT', you would map 'de-AT' to 'at' (ie. ['de-AT' => 'at']).
|
||||
'localesMapping' => [],
|
||||
|
||||
// Locale suffix for LC_TIME and LC_MONETARY
|
||||
// Defaults to most common ".UTF-8". Set to blank on Windows systems, change to ".utf8" on CentOS and similar.
|
||||
'utf8suffix' => env('LARAVELLOCALIZATION_UTF8SUFFIX', '.UTF-8'),
|
||||
|
||||
// URLs which should not be processed, e.g. '/nova', '/nova/*', '/nova-api/*' or specific application URLs
|
||||
// Defaults to [].
|
||||
'urlsIgnored' => ['/skipped'],
|
||||
|
||||
'httpMethodsIgnored' => ['POST', 'PUT', 'PATCH', 'DELETE'],
|
||||
];
|
||||
150
config/logging.php
Normal file
150
config/logging.php
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Default Log Channel
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This option defines the default log channel that is utilized to write
|
||||
* messages to your logs. The value provided here should match one of
|
||||
* the channels present in the list of "channels" configured below.
|
||||
*
|
||||
*/
|
||||
|
||||
'default' => env('LOG_CHANNEL', 'stack'),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Deprecations Log Channel
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This option controls the log channel that should be used to log warnings
|
||||
* regarding deprecated PHP and library features. This allows you to get
|
||||
* your application ready for upcoming major versions of dependencies.
|
||||
*
|
||||
*/
|
||||
|
||||
'deprecations' => [
|
||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||
],
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Log Channels
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Here you may configure the log channels for your application. Laravel
|
||||
* utilizes the Monolog PHP logging library, which includes a variety
|
||||
* of powerful log handlers and formatters that you're free to use.
|
||||
*
|
||||
* Available drivers: "single", "daily", "slack", "syslog",
|
||||
* "errorlog", "monolog", "custom", "stack"
|
||||
*
|
||||
*/
|
||||
|
||||
'channels' => [
|
||||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => env('LOG_DAILY_DAYS', 14),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||
'level' => env('LOG_LEVEL', 'critical'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'papertrail' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||
'handler_with' => [
|
||||
'host' => env('PAPERTRAIL_URL'),
|
||||
'port' => env('PAPERTRAIL_PORT'),
|
||||
'connectionString' => 'tls://' . env('PAPERTRAIL_URL') . ':' . env('PAPERTRAIL_PORT'),
|
||||
],
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'handler_with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'syslog' => [
|
||||
'driver' => 'syslog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'errorlog' => [
|
||||
'driver' => 'errorlog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
'emergency' => [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
'stdout' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'formatter' => env('LOG_STDOUT_FORMATTER'),
|
||||
'with' => [
|
||||
'stream' => 'php://stdout',
|
||||
],
|
||||
],
|
||||
|
||||
'k8s' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => ['daily', 'stdout'],
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
119
config/mail.php
Normal file
119
config/mail.php
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Default Mailer
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This option controls the default mailer that is used to send all email
|
||||
* messages unless another mailer is explicitly specified when sending
|
||||
* the message. All additional mailers can be configured within the
|
||||
* "mailers" array. Examples of each type of mailer are provided.
|
||||
*
|
||||
*/
|
||||
|
||||
'default' => env('MAIL_MAILER', 'smtp'),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Mailer Configurations
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Here you may configure all of the mailers used by your application plus
|
||||
* their respective settings. Several examples have been configured for
|
||||
* you and you are free to add your own as your application requires.
|
||||
*
|
||||
* Laravel supports a variety of mail "transport" drivers that can be used
|
||||
* when delivering an email. You may specify which one you're using for
|
||||
* your mailers below. You may also add additional mailers if needed.
|
||||
*
|
||||
* Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||
* "postmark", "resend", "log", "array",
|
||||
* "failover", "roundrobin"
|
||||
*
|
||||
*/
|
||||
|
||||
'mailers' => [
|
||||
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'scheme' => env('MAIL_SCHEME'),
|
||||
'url' => env('MAIL_URL'),
|
||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||
'port' => env('MAIL_PORT', 2525),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env(
|
||||
'MAIL_EHLO_DOMAIN',
|
||||
parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)
|
||||
),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'transport' => 'resend',
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'transport' => 'log',
|
||||
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'transport' => 'array',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'transport' => 'failover',
|
||||
'mailers' => [
|
||||
'smtp',
|
||||
'log',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => [
|
||||
'ses',
|
||||
'postmark',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Global "From" Address
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* You may wish for all emails sent by your application to be sent from
|
||||
* the same address. Here you may specify a name and address that is
|
||||
* used globally for all emails that are sent by your application.
|
||||
*
|
||||
*/
|
||||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||
],
|
||||
|
||||
];
|
||||
133
config/purifier.php
Normal file
133
config/purifier.php
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Ok, glad you are here
|
||||
* first we get a config instance, and set the settings
|
||||
* $config = HTMLPurifier_Config::createDefault();
|
||||
* $config->set('Core.Encoding', $this->config->get('purifier.encoding'));
|
||||
* $config->set('Cache.SerializerPath', $this->config->get('purifier.cachePath'));
|
||||
* if ( ! $this->config->get('purifier.finalize')) {
|
||||
* $config->autoFinalize = false;
|
||||
* }
|
||||
* $config->load [$this->getConfig());
|
||||
*
|
||||
* You must NOT delete the default settings
|
||||
* anything in settings should be compacted with params that needed to instance HTMLPurifier_Config.
|
||||
*
|
||||
* @link http://htmlpurifier.org/live/configdoc/plain.html
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// phpcs:disable Generic.Files.LineLength.TooLong
|
||||
return [
|
||||
'encoding' => 'UTF-8',
|
||||
'finalize' => true,
|
||||
'cachePath' => storage_path('app/purifier'),
|
||||
'cacheFileMode' => 0755,
|
||||
'settings' => [
|
||||
'default' => [
|
||||
'HTML.SafeIframe' => true,
|
||||
// * For iframes
|
||||
'URI.SafeIframeRegexp' => '%^(http://|https://|//)(www.youtube.com/embed/|player.vimeo.com/video/|vimeo.com/)%',
|
||||
'HTML.Doctype' => 'HTML 4.01 Transitional',
|
||||
// * Legacy filter en full avec safe legacy.
|
||||
// phpcs:ignore Generic.Files.LineLength.MaxExceeded
|
||||
'HTML.Allowed' => 'h1[style],h2[style],h3[style],h4[style],h5[style],h6[style],figure[class|style],figcaption,oembed[url],div[style|class],blockquote,b,strong,i,em,u,s,a[href|title|target|rel|data-model],ul[style|data-type],ol[style],dl[style],li[style|data-type],label,input[type|checked|disabled],p[style],br,span[style|class],img[width|height|alt|src|style|class|title],iframe[src|width|height|class|frameborder|style|allowfullscreen],table[style|align|border|cellspacing|cellpadding|width],thead,tbody,th[colspan|align|valign|style],tr[align|valign|style],td[style|colspan|rowspan|scope|align|valign],hr,dl,dt,sub,sup',
|
||||
// * Restricted basics.
|
||||
// 'HTML.Allowed' => 'br,p[style],b[style],u[style],i[style],a[href|title|target|rel],strong[style],span[class],ul[style],ol[style],li[style],h2[style],h3[style],h4[style],hr,blockquote[style]',
|
||||
// * Legacy filter en full avec safe legacy.
|
||||
// phpcs:ignore Generic.Files.LineLength.MaxExceeded
|
||||
'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,margin,margin-left,margin-right,color,background-color,text-align,list-style-type,float,width,height,vertical-align,border-color',
|
||||
// * Restricted basics.
|
||||
// 'CSS.AllowedProperties' => 'text-align,margin-left',
|
||||
'AutoFormat.AutoParagraph' => false,
|
||||
'AutoFormat.RemoveEmpty' => false,
|
||||
'AutoFormat.RemoveEmpty.RemoveNbsp' => false,
|
||||
],
|
||||
'custom_definition' => [
|
||||
'id' => 'html5-definitions',
|
||||
'rev' => 2,
|
||||
'debug' => true,
|
||||
'elements' => [
|
||||
// * http://developers.whatwg.org/sections.html
|
||||
['section', 'Block', 'Flow', 'Common'],
|
||||
['nav', 'Block', 'Flow', 'Common'],
|
||||
['article', 'Block', 'Flow', 'Common'],
|
||||
['aside', 'Block', 'Flow', 'Common'],
|
||||
['header', 'Block', 'Flow', 'Common'],
|
||||
['footer', 'Block', 'Flow', 'Common'],
|
||||
|
||||
// * Content model actually excludes several tags, not modelled here
|
||||
['address', 'Block', 'Flow', 'Common'],
|
||||
['hgroup', 'Block', 'Required: h1 | h2 | h3 | h4 | h5 | h6', 'Common'],
|
||||
|
||||
// * http://developers.whatwg.org/grouping-content.html
|
||||
['figure', 'Block', 'Optional: (figcaption, Flow) | (Flow, figcaption) | Flow', 'Common'],
|
||||
['figcaption', 'Inline', 'Flow', 'Common'],
|
||||
['oembed', 'Inline', 'Flow', 'Common'],
|
||||
|
||||
// * http://developers.whatwg.org/the-video-element.html#the-video-element
|
||||
['video', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', [
|
||||
'src' => 'URI',
|
||||
'type' => 'Text',
|
||||
'width' => 'Length',
|
||||
'height' => 'Length',
|
||||
'poster' => 'URI',
|
||||
'preload' => 'Enum#auto,metadata,none',
|
||||
'controls' => 'Bool',
|
||||
]],
|
||||
['source', 'Block', 'Flow', 'Common', [
|
||||
'src' => 'URI',
|
||||
'type' => 'Text',
|
||||
]],
|
||||
|
||||
// * http://developers.whatwg.org/text-level-semantics.html
|
||||
['s', 'Inline', 'Inline', 'Common'],
|
||||
['var', 'Inline', 'Inline', 'Common'],
|
||||
['sub', 'Inline', 'Inline', 'Common'],
|
||||
['sup', 'Inline', 'Inline', 'Common'],
|
||||
['mark', 'Inline', 'Inline', 'Common'],
|
||||
['wbr', 'Inline', 'Empty', 'Core'],
|
||||
|
||||
// * http://developers.whatwg.org/edits.html
|
||||
['ins', 'Block', 'Flow', 'Common', ['cite' => 'URI', 'datetime' => 'CDATA']],
|
||||
['del', 'Block', 'Flow', 'Common', ['cite' => 'URI', 'datetime' => 'CDATA']],
|
||||
],
|
||||
'attributes' => [
|
||||
['iframe', 'allowfullscreen', 'Bool'],
|
||||
['table', 'height', 'Text'],
|
||||
['td', 'border', 'Text'],
|
||||
['th', 'border', 'Text'],
|
||||
['tr', 'width', 'Text'],
|
||||
['tr', 'height', 'Text'],
|
||||
['tr', 'border', 'Text'],
|
||||
['figure', 'class', 'Text'],
|
||||
['figure', 'style', 'Text'],
|
||||
['oembed', 'url', 'Text'],
|
||||
['span', 'style', 'Text'],
|
||||
['img', 'style', 'Text'],
|
||||
],
|
||||
],
|
||||
'custom_attributes' => [
|
||||
['a', 'target', 'Enum#_blank,_self,_target,_top'],
|
||||
['a', 'data-model', 'Text'],
|
||||
['input', 'type', 'Text'],
|
||||
['input', 'checked', 'Bool'],
|
||||
['input', 'disabled', 'Bool'],
|
||||
['ol', 'style', 'Text'],
|
||||
['ul', 'data-type', 'Text'],
|
||||
['li', 'data-type', 'Text'],
|
||||
['figure', 'style', 'Text'],
|
||||
['img', 'style', 'Text'],
|
||||
['span', 'style', 'Text'],
|
||||
['oembed', 'url', 'Text'],
|
||||
],
|
||||
'custom_elements' => [
|
||||
['u', 'Inline', 'Inline', 'Common'],
|
||||
['oembed', 'Inline', 'Inline', 'Common'],
|
||||
['label', 'Inline', 'Flow', 'Common'],
|
||||
['input', 'Inline', 'Empty', 'Common'],
|
||||
],
|
||||
],
|
||||
];
|
||||
131
config/queue.php
Normal file
131
config/queue.php
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Default Queue Connection Name
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Laravel's queue supports a variety of backends via a single, unified
|
||||
* API, giving you convenient access to each backend using identical
|
||||
* syntax for each. The default queue connection is defined below.
|
||||
*
|
||||
*/
|
||||
|
||||
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Queue Connections
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Here you may configure the connection options for every queue backend
|
||||
* used by your application. An example configuration is provided for
|
||||
* each backend supported by Laravel. You're also free to add more.
|
||||
*
|
||||
* Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||
* "deferred", "background", "failover", "null"
|
||||
*
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||
'queue' => env('DB_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'beanstalkd' => [
|
||||
'driver' => 'beanstalkd',
|
||||
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => 0,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'sqs' => [
|
||||
'driver' => 'sqs',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||
'queue' => env('SQS_QUEUE', 'default'),
|
||||
'suffix' => env('SQS_SUFFIX'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||
'queue' => env('REDIS_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => null,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'deferred' => [
|
||||
'driver' => 'deferred',
|
||||
],
|
||||
|
||||
'background' => [
|
||||
'driver' => 'background',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'connections' => [
|
||||
'database',
|
||||
'deferred',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Job Batching
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* The following options configure the database and table that store job
|
||||
* batching information. These options can be updated to any database
|
||||
* connection and table which has been defined by your application.
|
||||
*
|
||||
*/
|
||||
|
||||
'batching' => [
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'job_batches',
|
||||
],
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Failed Queue Jobs
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* These options configure the behavior of failed queue job logging so you
|
||||
* can control how and where failed jobs are stored. Laravel ships with
|
||||
* support for storing failed jobs in a simple file or in a database.
|
||||
*
|
||||
* Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||
*
|
||||
*/
|
||||
|
||||
'failed' => [
|
||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'failed_jobs',
|
||||
],
|
||||
|
||||
];
|
||||
343
config/saml2.php
Normal file
343
config/saml2.php
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Slides\Saml2\Models\Tenant;
|
||||
|
||||
return [
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Tenant Model
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This will allow you to override the tenant model with your own.
|
||||
*/
|
||||
'tenantModel' => Tenant::class,
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Use built-in routes
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* If "useRoutes" set to true, the package defines five new routes:
|
||||
*
|
||||
* Method | URI | Name
|
||||
* -------|---------------------------------|------------------
|
||||
* POST | {routesPrefix}/{uuid}/acs | saml.acs
|
||||
* GET | {routesPrefix}/{uuid}/login | saml.login
|
||||
* GET | {routesPrefix}/{uuid}/logout | saml.logout
|
||||
* GET | {routesPrefix}/{uuid}/metadata | saml.metadata
|
||||
* GET | {routesPrefix}/{uuid}/sls | saml.sls
|
||||
*/
|
||||
'useRoutes' => true,
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Built-in routes prefix
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Here you may define the prefix for built-in routes.
|
||||
*/
|
||||
'routesPrefix' => '/saml2',
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Middle groups to use for the SAML routes
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Note, Laravel 5.2 requires a group which includes StartSession
|
||||
*/
|
||||
'routesMiddleware' => ['saml'],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Signature validation
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Set to true if you want to use parameters from $_SERVER to validate the signature.
|
||||
*/
|
||||
'retrieveParametersFromServer' => false,
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Login redirection URL.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* The redirection URL after successful login.
|
||||
*/
|
||||
'loginRoute' => '/admin/login',
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Logout redirection URL.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* The redirection URL after successful logout.
|
||||
*/
|
||||
'logoutRoute' => '/admin/login',
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Login error redirection URL.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* The redirection URL after login failing.
|
||||
*/
|
||||
'errorRoute' => '/admin/login',
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Strict mode.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* If 'strict' is True, then the PHP Toolkit will reject unsigned
|
||||
* or unencrypted messages if it expects them signed or encrypted
|
||||
* Also will reject the messages if not strictly follow the SAML
|
||||
* standard: Destination, NameId, Conditions... are validated too.
|
||||
*/
|
||||
'strict' => true,
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Debug mode.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* When enabled, errors must be printed.
|
||||
*/
|
||||
'debug' => env('SAML2_DEBUG', env('APP_DEBUG', false)),
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Whether to use `X-Forwarded-*` headers to determine port/domain/protocol.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* If 'proxyVars' is True, then the Saml lib will trust proxy headers
|
||||
* e.g X-Forwarded-Proto / HTTP_X_FORWARDED_PROTO. This is useful if
|
||||
* your application is running behind a load balancer which terminates SSL.
|
||||
*/
|
||||
'proxyVars' => true,
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Service Provider configuration.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* General setting of the service provider.
|
||||
*/
|
||||
'sp' => [
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* NameID format.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Specifies constraints on the name identifier to be used to
|
||||
* represent the requested subject.
|
||||
*/
|
||||
'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* SP Certificates.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Usually x509cert and privateKey of the SP are provided by files placed at
|
||||
* the certs folder. But we can also provide them with the following parameters.
|
||||
*/
|
||||
'x509cert' => env('SAML2_SP_CERT_x509', ''),
|
||||
'privateKey' => env('SAML2_SP_CERT_PRIVATEKEY', ''),
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Identifier (URI) of the SP entity.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Leave blank to use the 'saml.metadata' route.
|
||||
*/
|
||||
'entityId' => env('SAML2_SP_ENTITYID', ''),
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* The Assertion Consumer Service (ACS) URL.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* URL Location where the <Response> from the IdP will be returned, using HTTP-POST binding.
|
||||
* Leave blank to use the 'saml.acs' route.
|
||||
*/
|
||||
'assertionConsumerService' => [
|
||||
'url' => '',
|
||||
],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* The Single Logout Service URL.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Specifies info about where and how the <Logout Response> message MUST be
|
||||
* returned to the requester, in this case our SP.
|
||||
*
|
||||
* URL Location where the <Response> from the IdP will be returned, using HTTP-Redirect binding.
|
||||
* Leave blank to use the 'saml.sls' route.
|
||||
*/
|
||||
'singleLogoutService' => [
|
||||
'url' => '',
|
||||
],
|
||||
],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* OneLogin security settings.
|
||||
*--------------------------------------------------------------------------
|
||||
*/
|
||||
'security' => [
|
||||
|
||||
'signatureAlgorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
|
||||
'digestAlgorithm' => 'http://www.w3.org/2001/04/xmlenc#sha256',
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* NameId encryption
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Indicates that the nameID of the <samlp:logoutRequest> sent by this SP
|
||||
* will be encrypted.
|
||||
*/
|
||||
'nameIdEncrypted' => true,
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* AuthnRequest signage
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Indicates whether the <samlp:AuthnRequest> messages sent by
|
||||
* this SP will be signed. The Metadata of the SP will offer this info
|
||||
*/
|
||||
'authnRequestsSigned' => true,
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Logout request signage
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Indicates whether the <samlp:logoutRequest> messages sent by this SP
|
||||
* will be signed.
|
||||
*/
|
||||
'logoutRequestSigned' => true,
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Logout response signage
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Indicates whether the <samlp:logoutResponse> messages sent by this SP
|
||||
* will be signed.
|
||||
*/
|
||||
'logoutResponseSigned' => true,
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Whether need to sign metadata.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* The possible values:
|
||||
* - false
|
||||
* - true (use certs)
|
||||
* - array:
|
||||
* ```
|
||||
* [
|
||||
* 'keyFileName' => 'metadata.key',
|
||||
* 'certFileName' => 'metadata.crt'
|
||||
* ]
|
||||
* ```
|
||||
*/
|
||||
'signMetadata' => true,
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Requirement to sign messages.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Indicates a requirement for the <samlp:Response>, <samlp:LogoutRequest> and
|
||||
* <samlp:LogoutResponse> elements received by this SP to be signed.
|
||||
*/
|
||||
'wantMessagesSigned' => true,
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Requirement to sign assertion elements.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Indicates a requirement for the <saml:Assertion> elements received by
|
||||
* this SP to be signed.
|
||||
*/
|
||||
'wantAssertionsSigned' => true,
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Requirement to encrypt NameID.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Indicates a requirement for the NameID received by this SP to be encrypted.
|
||||
*/
|
||||
'wantNameIdEncrypted' => false,
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Authentication context.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Set to false and no AuthContext will be sent in the AuthNRequest,
|
||||
*
|
||||
* Set true or don't present this parameter and you will get an
|
||||
* AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'
|
||||
*
|
||||
* Set an array with the possible auth context values:
|
||||
* ['urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509']
|
||||
*/
|
||||
'requestedAuthnContext' => true,
|
||||
],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Contact information.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* It is recommended to supply a technical and support contacts.
|
||||
*/
|
||||
'contactPerson' => [
|
||||
'technical' => [
|
||||
'givenName' => env('SAML2_CONTACT_TECHNICAL_NAME', env('APP_NAME', 'Technical')),
|
||||
'emailAddress' => env('SAML2_CONTACT_TECHNICAL_EMAIL', 'no@reply.com'),
|
||||
],
|
||||
'support' => [
|
||||
'givenName' => env('SAML2_CONTACT_SUPPORT_NAME', env('APP_NAME', 'Support')),
|
||||
'emailAddress' => env('SAML2_CONTACT_SUPPORT_EMAIL', 'no@reply.com'),
|
||||
],
|
||||
],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Organization information.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* The info in en_US lang is recommended, add more if required.
|
||||
*/
|
||||
'organization' => [
|
||||
'en-US' => [
|
||||
'name' => env('SAML2_ORGANIZATION_NAME', env('APP_NAME', 'Name')),
|
||||
'displayname' => env('SAML2_ORGANIZATION_NAME', env('APP_NAME', 'Name')),
|
||||
'url' => env('SAML2_ORGANIZATION_URL', env('APP_URL', 'http://url')),
|
||||
],
|
||||
],
|
||||
|
||||
/**
|
||||
* Default tenant UUID.
|
||||
*/
|
||||
'default_tenant_uuid' => env('SAML2_DEFAULT_TENANT_UUID', null),
|
||||
|
||||
/**
|
||||
* Target domain.
|
||||
*/
|
||||
'domain' => env('SAML2_DOMAIN', ''),
|
||||
];
|
||||
212
config/scout.php
Normal file
212
config/scout.php
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Search Engine
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default search connection that gets used while
|
||||
| using Laravel Scout. This connection is used when syncing all models
|
||||
| to the search service. You should adjust this based on your needs.
|
||||
|
|
||||
| Supported: "algolia", "meilisearch", "typesense",
|
||||
| "database", "collection", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SCOUT_DRIVER', 'collection'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Index Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify a prefix that will be applied to all search index
|
||||
| names used by Scout. This prefix may be useful if you have multiple
|
||||
| "tenants" or applications sharing the same search infrastructure.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('SCOUT_PREFIX', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Data Syncing
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to control if the operations that sync your data
|
||||
| with your search engines are queued. When this is set to "true" then
|
||||
| all automatic data syncing will get queued for better performance.
|
||||
|
|
||||
*/
|
||||
|
||||
'queue' => env('SCOUT_QUEUE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Transactions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This configuration option determines if your data will only be synced
|
||||
| with your search indexes after every open database transaction has
|
||||
| been committed, thus preventing any discarded data from syncing.
|
||||
|
|
||||
*/
|
||||
|
||||
'after_commit' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Chunk Sizes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options allow you to control the maximum chunk size when you are
|
||||
| mass importing data into the search engine. This allows you to fine
|
||||
| tune each of these chunk sizes based on the power of the servers.
|
||||
|
|
||||
*/
|
||||
|
||||
'chunk' => [
|
||||
'searchable' => 500,
|
||||
'unsearchable' => 500,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Soft Deletes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows to control whether to keep soft deleted records in
|
||||
| the search indexes. Maintaining soft deleted records can be useful
|
||||
| if your application still needs to search for the records later.
|
||||
|
|
||||
*/
|
||||
|
||||
'soft_delete' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Identify User
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to control whether to notify the search engine
|
||||
| of the user performing the search. This is sometimes useful if the
|
||||
| engine supports any analytics based on this application's users.
|
||||
|
|
||||
| Supported engines: "algolia"
|
||||
|
|
||||
*/
|
||||
|
||||
'identify' => env('SCOUT_IDENTIFY', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Algolia Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Algolia settings. Algolia is a cloud hosted
|
||||
| search engine which works great with Scout out of the box. Just plug
|
||||
| in your application ID and admin API key to get started searching.
|
||||
|
|
||||
*/
|
||||
|
||||
'algolia' => [
|
||||
'id' => env('ALGOLIA_APP_ID', ''),
|
||||
'secret' => env('ALGOLIA_SECRET', ''),
|
||||
'index-settings' => [
|
||||
// 'users' => [
|
||||
// 'searchableAttributes' => ['id', 'name', 'email'],
|
||||
// 'attributesForFaceting'=> ['filterOnly(email)'],
|
||||
// ],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Meilisearch Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Meilisearch settings. Meilisearch is an open
|
||||
| source search engine with minimal configuration. Below, you can state
|
||||
| the host and key information for your own Meilisearch installation.
|
||||
|
|
||||
| See: https://www.meilisearch.com/docs/learn/configuration/instance_options#all-instance-options
|
||||
|
|
||||
*/
|
||||
|
||||
'meilisearch' => [
|
||||
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
|
||||
'key' => env('MEILISEARCH_KEY'),
|
||||
'index-settings' => [
|
||||
// 'users' => [
|
||||
// 'filterableAttributes'=> ['id', 'name', 'email'],
|
||||
// ],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Typesense Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Typesense settings. Typesense is an open
|
||||
| source search engine using minimal configuration. Below, you will
|
||||
| state the host, key, and schema configuration for the instance.
|
||||
|
|
||||
*/
|
||||
|
||||
'typesense' => [
|
||||
'client-settings' => [
|
||||
'api_key' => env('TYPESENSE_API_KEY', 'xyz'),
|
||||
'nodes' => [
|
||||
[
|
||||
'host' => env('TYPESENSE_HOST', 'localhost'),
|
||||
'port' => env('TYPESENSE_PORT', '8108'),
|
||||
'path' => env('TYPESENSE_PATH', ''),
|
||||
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
|
||||
],
|
||||
],
|
||||
'nearest_node' => [
|
||||
'host' => env('TYPESENSE_HOST', 'localhost'),
|
||||
'port' => env('TYPESENSE_PORT', '8108'),
|
||||
'path' => env('TYPESENSE_PATH', ''),
|
||||
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
|
||||
],
|
||||
'connection_timeout_seconds' => env('TYPESENSE_CONNECTION_TIMEOUT_SECONDS', 2),
|
||||
'healthcheck_interval_seconds' => env('TYPESENSE_HEALTHCHECK_INTERVAL_SECONDS', 30),
|
||||
'num_retries' => env('TYPESENSE_NUM_RETRIES', 3),
|
||||
'retry_interval_seconds' => env('TYPESENSE_RETRY_INTERVAL_SECONDS', 1),
|
||||
],
|
||||
// 'max_total_results' => env('TYPESENSE_MAX_TOTAL_RESULTS', 1000),
|
||||
'model-settings' => [
|
||||
// User::class => [
|
||||
// 'collection-schema' => [
|
||||
// 'fields' => [
|
||||
// [
|
||||
// 'name' => 'id',
|
||||
// 'type' => 'string',
|
||||
// ],
|
||||
// [
|
||||
// 'name' => 'name',
|
||||
// 'type' => 'string',
|
||||
// ],
|
||||
// [
|
||||
// 'name' => 'created_at',
|
||||
// 'type' => 'int64',
|
||||
// ],
|
||||
// ],
|
||||
// 'default_sorting_field' => 'created_at',
|
||||
// ],
|
||||
// 'search-parameters' => [
|
||||
// 'query_by' => 'name'
|
||||
// ],
|
||||
// ],
|
||||
],
|
||||
'import_action' => env('TYPESENSE_IMPORT_ACTION', 'upsert'),
|
||||
],
|
||||
|
||||
];
|
||||
65
config/sentry.php
Normal file
65
config/sentry.php
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Uwil\LaravelCommon\Helpers\ExceptionsHelper;
|
||||
|
||||
return [
|
||||
|
||||
'dsn' => env('SENTRY_LARAVEL_DSN', env('SENTRY_DSN')),
|
||||
|
||||
// The release version of your application.
|
||||
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')).
|
||||
'release' => env('SENTRY_RELEASE'),
|
||||
|
||||
// When left empty or `null` the Laravel environment will be used.
|
||||
'environment' => env('SENTRY_ENVIRONMENT'),
|
||||
|
||||
'breadcrumbs' => [
|
||||
// Capture Laravel logs in breadcrumbs.
|
||||
'logs' => true,
|
||||
|
||||
// Capture SQL queries in breadcrumbs.
|
||||
'sql_queries' => true,
|
||||
|
||||
// Capture bindings on SQL queries logged in breadcrumbs.
|
||||
'sql_bindings' => false,
|
||||
|
||||
// Capture queue job information in breadcrumbs.
|
||||
'queue_info' => true,
|
||||
|
||||
// Capture command information in breadcrumbs.
|
||||
'command_info' => true,
|
||||
],
|
||||
|
||||
'tracing' => [
|
||||
// Trace queue jobs as their own transactions.
|
||||
'queue_job_transactions' => env('SENTRY_TRACE_QUEUE_ENABLED', false),
|
||||
|
||||
// Capture queue jobs as spans when executed on the sync driver.
|
||||
'queue_jobs' => true,
|
||||
|
||||
// Capture SQL queries as spans.
|
||||
'sql_queries' => true,
|
||||
|
||||
// Try to find out where the SQL query originated from and add it to the query spans.
|
||||
'sql_origin' => true,
|
||||
|
||||
// Capture views as spans.
|
||||
'views' => true,
|
||||
|
||||
// Indicates if the tracing integrations supplied by Sentry should be loaded.
|
||||
'default_integrations' => true,
|
||||
|
||||
// Indicates that requests without a matching route should be traced.
|
||||
'missing_routes' => false,
|
||||
],
|
||||
|
||||
// See: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#send-default-pii.
|
||||
'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false),
|
||||
|
||||
// See: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces-sample-rate.
|
||||
'traces_sample_rate' => env('SENTRY_TRACES_SAMPLE_RATE') === null ? null : (float) env('SENTRY_TRACES_SAMPLE_RATE'),
|
||||
|
||||
'before_send' => [ExceptionsHelper::class, 'sentryEventHandler'],
|
||||
];
|
||||
47
config/services.php
Normal file
47
config/services.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Third Party Services
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This file is for storing the credentials for third party services such
|
||||
* as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||
* location for this type of information, allowing packages to have
|
||||
* a conventional file to locate the various service credentials.
|
||||
*
|
||||
*/
|
||||
|
||||
'postmark' => [
|
||||
'key' => env('POSTMARK_API_KEY'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_API_KEY'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'notifications' => [
|
||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||
],
|
||||
],
|
||||
|
||||
'google' => [
|
||||
'recaptcha' => [
|
||||
'public' => env('GOOGLE_RECAPTCHA_PUBLIC'),
|
||||
'private' => env('GOOGLE_RECAPTCHA_PRIVATE'),
|
||||
'version' => env('GOOGLE_RECAPTCHA_VERSION', '3') === '3' ? true : false,
|
||||
],
|
||||
],
|
||||
];
|
||||
219
config/session.php
Normal file
219
config/session.php
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Default Session Driver
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This option determines the default session driver that is utilized for
|
||||
* incoming requests. Laravel supports a variety of storage options to
|
||||
* persist session data. Database storage is a great default choice.
|
||||
*
|
||||
* Supported: "file", "cookie", "database", "memcached",
|
||||
* "redis", "dynamodb", "array"
|
||||
*
|
||||
*/
|
||||
|
||||
'driver' => env('SESSION_DRIVER', 'database'),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Session Lifetime
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Here you may specify the number of minutes that you wish the session
|
||||
* to be allowed to remain idle before it expires. If you want them
|
||||
* to expire immediately when the browser is closed then you may
|
||||
* indicate that via the expire_on_close configuration option.
|
||||
*
|
||||
*/
|
||||
|
||||
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||
|
||||
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Session Encryption
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This option allows you to easily specify that all of your session data
|
||||
* should be encrypted before it's stored. All encryption is performed
|
||||
* automatically by Laravel and you may use the session like normal.
|
||||
*
|
||||
*/
|
||||
|
||||
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Session File Location
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* When utilizing the "file" session driver, the session files are placed
|
||||
* on disk. The default storage location is defined here; however, you
|
||||
* are free to provide another location where they should be stored.
|
||||
*
|
||||
*/
|
||||
|
||||
'files' => storage_path('framework/sessions'),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Session Database Connection
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* When using the "database" or "redis" session drivers, you may specify a
|
||||
* connection that should be used to manage these sessions. This should
|
||||
* correspond to a connection in your database configuration options.
|
||||
*
|
||||
*/
|
||||
|
||||
'connection' => env('SESSION_CONNECTION'),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Session Database Table
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* When using the "database" session driver, you may specify the table to
|
||||
* be used to store sessions. Of course, a sensible default is defined
|
||||
* for you; however, you're welcome to change this to another table.
|
||||
*
|
||||
*/
|
||||
|
||||
'table' => env('SESSION_TABLE', 'sessions'),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Session Cache Store
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* When using one of the framework's cache driven session backends, you may
|
||||
* define the cache store which should be used to store the session data
|
||||
* between requests. This must match one of your defined cache stores.
|
||||
*
|
||||
* Affects: "dynamodb", "memcached", "redis"
|
||||
*
|
||||
*/
|
||||
|
||||
'store' => env('SESSION_STORE'),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Session Sweeping Lottery
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Some session drivers must manually sweep their storage location to get
|
||||
* rid of old sessions from storage. Here are the chances that it will
|
||||
* happen on a given request. By default, the odds are 2 out of 100.
|
||||
*
|
||||
*/
|
||||
|
||||
'lottery' => [2, 100],
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Session Cookie Name
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Here you may change the name of the session cookie that is created by
|
||||
* the framework. Typically, you should not need to change this value
|
||||
* since doing so does not grant a meaningful security improvement.
|
||||
*
|
||||
*/
|
||||
|
||||
'cookie' => env(
|
||||
'SESSION_COOKIE',
|
||||
Str::slug((string) env('APP_NAME', 'laravel')) . '-session'
|
||||
),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Session Cookie Path
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* The session cookie path determines the path for which the cookie will
|
||||
* be regarded as available. Typically, this will be the root path of
|
||||
* your application, but you're free to change this when necessary.
|
||||
*
|
||||
*/
|
||||
|
||||
'path' => env('SESSION_PATH', '/'),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Session Cookie Domain
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This value determines the domain and subdomains the session cookie is
|
||||
* available to. By default, the cookie will be available to the root
|
||||
* domain without subdomains. Typically, this shouldn't be changed.
|
||||
*
|
||||
*/
|
||||
|
||||
'domain' => env('SESSION_DOMAIN'),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* HTTPS Only Cookies
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* By setting this option to true, session cookies will only be sent back
|
||||
* to the server if the browser has a HTTPS connection. This will keep
|
||||
* the cookie from being sent to you when it can't be done securely.
|
||||
*
|
||||
*/
|
||||
|
||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* HTTP Access Only
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Setting this value to true will prevent JavaScript from accessing the
|
||||
* value of the cookie and the cookie will only be accessible through
|
||||
* the HTTP protocol. It's unlikely you should disable this option.
|
||||
*
|
||||
*/
|
||||
|
||||
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Same-Site Cookies
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This option determines how your cookies behave when cross-site requests
|
||||
* take place, and can be used to mitigate CSRF attacks. By default, we
|
||||
* will set this value to "lax" to permit secure cross-site requests.
|
||||
*
|
||||
* See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||
*
|
||||
* Supported: "lax", "strict", "none", null
|
||||
*
|
||||
*/
|
||||
|
||||
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||
|
||||
/*
|
||||
*--------------------------------------------------------------------------
|
||||
* Partitioned Cookies
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* Setting this value to true will tie the cookie to the top-level site for
|
||||
* a cross-site context. Partitioned cookies are accepted by the browser
|
||||
* when flagged "secure" and the Same-Site attribute is set to "none".
|
||||
*
|
||||
*/
|
||||
|
||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||
|
||||
];
|
||||
170
config/telescope.php
Normal file
170
config/telescope.php
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Laravel\Telescope\Http\Middleware\Authorize;
|
||||
use Laravel\Telescope\Watchers;
|
||||
|
||||
return [
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Telescope Master Switch
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This option may be used to disable all Telescope watchers regardless
|
||||
* of their individual configuration, which simply provides a single
|
||||
* and convenient way to enable or disable Telescope data storage.
|
||||
*/
|
||||
'enabled' => env('TELESCOPE_ENABLED', false),
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Telescope Domain
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This is the subdomain where Telescope will be accessible from. If the
|
||||
* setting is null, Telescope will reside under the same domain as the
|
||||
* application. Otherwise, this value will be used as the subdomain.
|
||||
*/
|
||||
'domain' => env('TELESCOPE_DOMAIN'),
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Telescope Path
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This is the URI path where Telescope will be accessible from. Feel free
|
||||
* to change this path to anything you like. Note that the URI will not
|
||||
* affect the paths of its internal API that aren't exposed to users.
|
||||
*/
|
||||
'path' => env('TELESCOPE_PATH', 'admin/telescope'),
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Telescope Storage Driver
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This configuration options determines the storage driver that will
|
||||
* be used to store Telescope's data. In addition, you may set any
|
||||
* custom options as needed by the particular driver you choose.
|
||||
*/
|
||||
'driver' => env('TELESCOPE_DRIVER', 'database'),
|
||||
|
||||
'storage' => [
|
||||
'database' => [
|
||||
'connection' => env('DB_CONNECTION', 'mysql'),
|
||||
'chunk' => 1000,
|
||||
],
|
||||
],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Telescope Queue
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This configuration options determines the queue connection and queue
|
||||
* which will be used to process ProcessPendingUpdate jobs. This can
|
||||
* be changed if you would prefer to use a non-default connection.
|
||||
*/
|
||||
'queue' => [
|
||||
'connection' => env('TELESCOPE_QUEUE_CONNECTION', null),
|
||||
'queue' => env('TELESCOPE_QUEUE', null),
|
||||
],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Telescope Route Middleware
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* These middleware will be assigned to every Telescope route, giving you
|
||||
* the chance to add your own middleware to this list or change any of
|
||||
* the existing middleware. Or, you can simply stick with this list.
|
||||
*/
|
||||
'middleware' => [
|
||||
Authorize::class,
|
||||
],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Allowed / Ignored Paths & Commands
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* The following array lists the URI paths and Artisan commands that will
|
||||
* not be watched by Telescope. In addition to this list, some Laravel
|
||||
* commands, like migrations and queue commands, are always ignored.
|
||||
*/
|
||||
'only_paths' => [],
|
||||
|
||||
'ignore_paths' => [
|
||||
'livewire*',
|
||||
'nova-api*',
|
||||
'pulse*',
|
||||
],
|
||||
|
||||
'ignore_commands' => [],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Telescope Watchers
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* The following array lists the "watchers" that will be registered with
|
||||
* Telescope. The watchers gather the application's profile data when
|
||||
* a request or task is executed. Feel free to customize this list.
|
||||
*/
|
||||
'watchers' => [
|
||||
Watchers\BatchWatcher::class => env('TELESCOPE_BATCH_WATCHER', true),
|
||||
Watchers\CacheWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_CACHE_WATCHER', true),
|
||||
'hidden' => [],
|
||||
],
|
||||
Watchers\ClientRequestWatcher::class => env('TELESCOPE_CLIENT_REQUEST_WATCHER', true),
|
||||
Watchers\CommandWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_COMMAND_WATCHER', true),
|
||||
'ignore' => [],
|
||||
],
|
||||
Watchers\DumpWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_DUMP_WATCHER', true),
|
||||
'always' => env('TELESCOPE_DUMP_WATCHER_ALWAYS', false),
|
||||
],
|
||||
Watchers\EventWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_EVENT_WATCHER', true),
|
||||
'ignore' => [],
|
||||
],
|
||||
Watchers\ExceptionWatcher::class => env('TELESCOPE_EXCEPTION_WATCHER', true),
|
||||
Watchers\GateWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_GATE_WATCHER', true),
|
||||
'ignore_abilities' => [],
|
||||
'ignore_packages' => true,
|
||||
'ignore_paths' => [],
|
||||
],
|
||||
Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', true),
|
||||
Watchers\LogWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_LOG_WATCHER', true),
|
||||
'level' => 'error',
|
||||
],
|
||||
Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', true),
|
||||
Watchers\ModelWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_MODEL_WATCHER', true),
|
||||
'events' => ['eloquent.*'],
|
||||
'hydrations' => true,
|
||||
],
|
||||
Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', true),
|
||||
Watchers\QueryWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_QUERY_WATCHER', true),
|
||||
'ignore_packages' => true,
|
||||
'ignore_paths' => [],
|
||||
'slow' => 100,
|
||||
],
|
||||
Watchers\RedisWatcher::class => env('TELESCOPE_REDIS_WATCHER', true),
|
||||
Watchers\RequestWatcher::class => [
|
||||
'enabled' => env('TELESCOPE_REQUEST_WATCHER', true),
|
||||
'size_limit' => env('TELESCOPE_RESPONSE_SIZE_LIMIT', 64),
|
||||
'ignore_http_methods' => [],
|
||||
'ignore_status_codes' => [],
|
||||
],
|
||||
Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', true),
|
||||
Watchers\ViewWatcher::class => env('TELESCOPE_VIEW_WATCHER', true),
|
||||
],
|
||||
];
|
||||
64
config/uwil-backend.php
Normal file
64
config/uwil-backend.php
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Uwil\LaravelBackend\Models\User;
|
||||
use Uwil\LaravelBackend\Requests\Users\StoreUserRequest;
|
||||
use Uwil\LaravelBackend\Requests\Users\UpdateUserRequest;
|
||||
|
||||
return [
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Routes
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This option may be used to edit routes informations.
|
||||
* - prefix: string.
|
||||
* - name: string.
|
||||
*/
|
||||
'routes' => [
|
||||
'prefix' => 'admin',
|
||||
'name' => 'back.',
|
||||
],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Redirects
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This option may be used to edit redirects.
|
||||
* - redirect_logout: string.
|
||||
*/
|
||||
'redirect_logout' => 'front.dynamic-pages.accueil.fr',
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Saml
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This option may be used to active or not the saml connection.
|
||||
* - enabled: boolean.
|
||||
*/
|
||||
'saml' => [
|
||||
'enabled' => true,
|
||||
],
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* User
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This options may be used when overriding the User model.
|
||||
* - model: class.
|
||||
* - store: class.
|
||||
* - update: class.
|
||||
*/
|
||||
'user' => [
|
||||
'model' => User::class,
|
||||
'requests' => [
|
||||
'store' => StoreUserRequest::class,
|
||||
'update' => UpdateUserRequest::class,
|
||||
],
|
||||
],
|
||||
];
|
||||
16
config/uwil-dynamic-pages.php
Normal file
16
config/uwil-dynamic-pages.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
/**
|
||||
*--------------------------------------------------------------------------
|
||||
* Path of the default view.
|
||||
*--------------------------------------------------------------------------
|
||||
*
|
||||
* This option may be used to edit routes informations.
|
||||
* - default-view: string.
|
||||
*/
|
||||
'default-view' => 'front/Pages/DynamicPageDefault',
|
||||
];
|
||||
76
database/factories/ProductFactory.php
Normal file
76
database/factories/ProductFactory.php
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Product;
|
||||
use Domain\Models\ValueObjects\Products\BreedingMethod;
|
||||
use Domain\Models\ValueObjects\Products\Business;
|
||||
use Domain\Models\ValueObjects\Products\NutritionalValues;
|
||||
use Domain\Models\ValueObjects\Products\ProductCategory;
|
||||
use Domain\Models\ValueObjects\Products\ProductFormat;
|
||||
use Domain\Models\ValueObjects\Products\Technology;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/** @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Product> */
|
||||
class ProductFactory extends Factory
|
||||
{
|
||||
protected $model = Product::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'seo_title' => $this->faker->sentence(4),
|
||||
'seo_description' => $this->faker->text(150),
|
||||
'picture' => $this->faker->imageUrl(800, 600, 'food'),
|
||||
'picture_alt' => $this->faker->words(3, true),
|
||||
'picture_title' => $this->faker->words(3, true),
|
||||
|
||||
'name' => ucfirst($this->faker->words(3, true)),
|
||||
|
||||
'category' => $this->faker->randomElement(ProductCategory::cases()),
|
||||
'technology' => $this->faker->randomElement(Technology::cases()),
|
||||
|
||||
'breeding_methods' => $this->faker->randomElements(
|
||||
BreedingMethod::cases(),
|
||||
$this->faker->numberBetween(1, 3)
|
||||
),
|
||||
'businesses' => $this->faker->randomElements(
|
||||
Business::cases(),
|
||||
$this->faker->numberBetween(1, count(Business::cases()))
|
||||
),
|
||||
|
||||
'is_egalim' => $this->faker->boolean(30),
|
||||
|
||||
'formats' => $this->faker->randomElements(
|
||||
ProductFormat::cases(),
|
||||
$this->faker->numberBetween(1, 3)
|
||||
),
|
||||
|
||||
'description' => $this->faker->paragraph(),
|
||||
'details_and_advantages' => $this->faker->paragraphs(2, true),
|
||||
'handling_advices' => $this->faker->paragraph(),
|
||||
'ingredients' => $this->faker->optional(0.8)->paragraph(),
|
||||
|
||||
'conservation_before_opening' => 'À conserver entre 0°C et 4°C.',
|
||||
'conservation_after_opening' => $this->faker->optional(0.7)->sentence(),
|
||||
|
||||
'nutritional_values' => $this->faker->boolean(80) ? new NutritionalValues(
|
||||
refWeightG: 100.0,
|
||||
kj: $this->faker->randomFloat(2, 0, 3000),
|
||||
kcal: $this->faker->randomFloat(2, 0, 800),
|
||||
fatG: $this->faker->randomFloat(2, 0, 100),
|
||||
satFatG: $this->faker->randomFloat(2, 0, 50),
|
||||
carbG: $this->faker->randomFloat(2, 0, 100),
|
||||
sugarG: $this->faker->randomFloat(2, 0, 100),
|
||||
fiberG: $this->faker->randomFloat(2, 0, 50),
|
||||
proteinG: $this->faker->randomFloat(2, 0, 100),
|
||||
saltG: $this->faker->randomFloat(2, 0, 10),
|
||||
) : null,
|
||||
|
||||
'pdf' => $this->faker->optional(0.5)->filePath(),
|
||||
'inco_code' => $this->faker->optional(0.5)->bothify('INCO-####'),
|
||||
];
|
||||
}
|
||||
}
|
||||
39
database/factories/RecipeFactory.php
Normal file
39
database/factories/RecipeFactory.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
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\Factories\Factory;
|
||||
|
||||
class RecipeFactory extends Factory
|
||||
{
|
||||
protected $model = Recipe::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'seo_title' => $this->faker->sentence(4),
|
||||
'seo_description' => $this->faker->text(150),
|
||||
'picture' => $this->faker->imageUrl(),
|
||||
'picture_alt' => $this->faker->words(3, true),
|
||||
'picture_title' => $this->faker->words(3, true),
|
||||
'name' => $this->faker->words(3, true),
|
||||
'product_category' => $this->faker->randomElement(ProductCategory::cases()),
|
||||
'consomption_time' => $this->faker->randomElement(ConsumptionTime::cases()),
|
||||
'inspiration' => $this->faker->randomElement(Inspiration::cases()),
|
||||
'season' => $this->faker->randomElement(Season::cases()),
|
||||
'description' => $this->faker->paragraph(),
|
||||
'details' => 'Pour ' . $this->faker->numberBetween(1, 10) . " personnes\n"
|
||||
. 'À réaliser en ' . $this->faker->numberBetween(15, 60) . ' minutes',
|
||||
'ingredients_list' => $this->faker->paragraph(),
|
||||
'preparation_steps' => $this->faker->text(),
|
||||
'tips_and_advices' => $this->faker->optional()->paragraph(),
|
||||
];
|
||||
}
|
||||
}
|
||||
56
database/migrations/2026_07_02_114944_create_products.php
Normal file
56
database/migrations/2026_07_02_114944_create_products.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class () extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('products', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
|
||||
$table->string('seo_title');
|
||||
$table->string('seo_description');
|
||||
$table->string('picture');
|
||||
$table->string('picture_alt');
|
||||
$table->string('picture_title');
|
||||
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
|
||||
$table->string('category')->comment('Domain\Models\ValueObjects\Products\ProductRange');
|
||||
$table->string('technology')->comment('Domain\Models\ValueObjects\Products\Technology');
|
||||
|
||||
$table->boolean('is_egalim')->default(false);
|
||||
|
||||
$table->json('breeding_methods')->comment('Array of Domain\Models\ValueObjects\Products\BreedingMethod');
|
||||
$table->json('businesses')->comment('Array of Domain\Models\ValueObjects\Products\Business');
|
||||
$table->json('formats')->comment('Array of Domain\Models\ValueObjects\Products\ProductFormat');
|
||||
|
||||
$table->text('description');
|
||||
$table->text('details_and_advantages');
|
||||
$table->text('handling_advices');
|
||||
$table->text('ingredients')->nullable();
|
||||
|
||||
$table->text('conservation_before_opening');
|
||||
$table->text('conservation_after_opening')->nullable();
|
||||
|
||||
$table->json('nutritional_values')->nullable()->comment('Domain\Models\ValueObjects\NutritionalValues');
|
||||
|
||||
$table->string('pdf')->nullable();
|
||||
|
||||
$table->softDeletes();
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('products');
|
||||
}
|
||||
};
|
||||
48
database/migrations/2026_07_02_114954_create_recipes.php
Normal file
48
database/migrations/2026_07_02_114954_create_recipes.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class () extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('recipes', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
|
||||
// SEO & Medias
|
||||
$table->string('seo_title');
|
||||
$table->string('seo_description');
|
||||
$table->string('picture');
|
||||
$table->string('picture_alt');
|
||||
$table->string('picture_title');
|
||||
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
|
||||
$table->string('product_category')->comment('Domain\Models\ValueObjects\Products\ProductRange');
|
||||
$table->string('consomption_time')->comment('Domain\Models\ValueObjects\Recipes\ConsumptionTime');
|
||||
$table->string('inspiration')->comment('Domain\Models\ValueObjects\Recipes\Inspiration');
|
||||
$table->string('season')->comment('Domain\Models\ValueObjects\Recipes\Season');
|
||||
|
||||
$table->text('description')->nullable();
|
||||
$table->text('details')->nullable();
|
||||
|
||||
$table->text('ingredients_list');
|
||||
$table->text('preparation_steps');
|
||||
$table->text('tips_and_advices')->nullable();
|
||||
|
||||
$table->softDeletes();
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('recipes');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class () extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('product_recipe', function (Blueprint $table) {
|
||||
$table->foreignUuid('product_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignUuid('recipe_id')->constrained()->cascadeOnDelete();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('product_recipe');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class () extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('activity_logs', function (Blueprint $table) {
|
||||
$table->string('model_id')->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class () extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('users', 'deleted_at')) {
|
||||
$table->softDeletes()->nullable()->comment('Date the user was deleted.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table): void {
|
||||
if (Schema::hasColumn('users', 'deleted_at')) {
|
||||
$table->dropSoftDeletes();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue