Compare commits

..

85 commits

Author SHA1 Message Date
Vincent BRIDIER
317577fb9b Merge branch 'issue-49' into 'develop'
Comments are now duplicated with products

See merge request ldc-web/projets/startcoq!37
2023-10-20 14:47:00 +00:00
Vincent BRIDIER
2a2490b302 Merge branch 'issue-58' into 'develop'
Inverted dates fields in filters

See merge request ldc-web/projets/startcoq!36
2023-10-20 14:46:25 +00:00
Vincent BRIDIER
53524b96d0 Merge branch 'issue-56' into 'develop'
Source details aren't required anymore

See merge request ldc-web/projets/startcoq!35
2023-10-20 14:45:46 +00:00
Vincent BRIDIER
e7fe9d920b Merge branch 'issue-55' into 'develop'
Gencod check is now called at every input change

See merge request ldc-web/projets/startcoq!33
2023-10-20 14:28:57 +00:00
Vincent BRIDIER
6e678b88c9 Merge branch 'issue-57' into 'develop'
Fixed BelongsToDropdown component: crashed when there was no value

See merge request ldc-web/projets/startcoq!32
2023-10-20 14:27:31 +00:00
Vincent BRIDIER
9c5cef6d99 Merge branch 'issue-51' into 'develop'
Comments are now open in edition form

See merge request ldc-web/projets/startcoq!31
2023-10-20 14:26:17 +00:00
noah.lancien
0ab895e7ac Comments are now open in edition form 2023-10-20 14:26:16 +00:00
Vincent BRIDIER
06301a44fa Merge branch 'issue-48' into 'develop'
Removed hours for seen_at displaying in pdf

See merge request ldc-web/projets/startcoq!30
2023-10-20 14:25:41 +00:00
Vincent BRIDIER
4274b21eea Merge branch 'issue-53' into 'develop'
Displayed specified claims in pdf and fixed specified claims update

See merge request ldc-web/projets/startcoq!29
2023-10-20 14:25:17 +00:00
Vincent BRIDIER
dec444160b Merge branch 'issue-46' into 'develop'
Added the weight unit in pdf

See merge request ldc-web/projets/startcoq!28
2023-10-20 14:24:33 +00:00
Vincent BRIDIER
4dfa2d0412 Merge branch 'issue-44' into 'develop'
Certifications are now visible in pdf

See merge request ldc-web/projets/startcoq!27
2023-10-20 14:24:01 +00:00
Vincent BRIDIER
a230697de0 Merge branch 'issue-32-allow-images-in-documents' into 'develop'
Issue 32 allow images in documents

Closes #32

See merge request ldc-web/projets/startcoq!24
2023-10-20 14:23:22 +00:00
Noah Lancien
2a9e109855 Comments are now duplicated with products 2023-10-17 10:12:05 +02:00
Noah Lancien
e19964fc89 Inverted dates fields in filters 2023-10-03 09:23:46 +02:00
Noah Lancien
edad0b4279 Source details aren't required anymore 2023-10-02 16:15:08 +00:00
Jérémy Munsch
d70135b6af
Migration fixes 2023-10-02 16:00:50 +02:00
Jérémy Munsch
adae4da02f
Fix enums valus with DB 2023-10-02 15:38:56 +02:00
Noah Lancien
2f7289f531 Gencod check is now called at every input change 2023-10-02 15:00:01 +02:00
Noah Lancien
6b85d63f5e Fixed BelongsToDropdown component: crashed when there was no value 2023-10-02 14:28:16 +02:00
Noah Lancien
5c5b34f33e Removed hours for seen_at displaying in pdf 2023-10-02 12:49:44 +02:00
Noah Lancien
ca47520a74 Displayed specified claims in pdf and fixed specified claims update 2023-10-02 12:34:49 +02:00
Noah Lancien
af3a63e36f Added the weight unit in pdf 2023-10-02 09:24:59 +02:00
Noah Lancien
21cc60457d Certifications are now visible in pdf 2023-10-02 09:20:59 +02:00
Vincent BRIDIER
36703bc9ac Merge branch 'issue-52-comments-placeholder' into 'develop'
Removed placeholders for comments

See merge request ldc-web/projets/startcoq!26
2023-09-29 16:22:16 +00:00
Noah Lancien
8a99cf98fc Removed placeholders for comments 2023-09-29 18:10:30 +02:00
Vincent BRIDIER
a4ed3dbb0f Merge branch 'issue-59' into 'develop'
Products are now ordered by 'created_at' desc

Closes #59

See merge request ldc-web/projets/startcoq!25
2023-09-29 15:59:50 +00:00
Noah Lancien
dae0c6271a Products are now ordered by 'created_at' desc 2023-09-29 17:57:09 +02:00
Jérémy Munsch
98762d68a8
Fix date sort on products 2023-09-18 11:55:53 +02:00
Jérémy Munsch
c450d71bed
Set max file sizes on products 2023-09-11 17:18:29 +02:00
Jérémy Munsch
99f7545595
Closes #32 Multiple file inputs for all doc types 2023-09-11 16:55:46 +02:00
Jérémy Munsch
a0ddeda5a9
pagination upgrade 2023-09-11 15:07:25 +02:00
Jérémy Munsch
771978412a
Fix keyword seeder (silent fail on Required keywords) 2023-09-11 12:28:44 +02:00
Jérémy Munsch
f43ea7c9ff
phpcs fixes 2023-09-11 12:22:32 +02:00
Jérémy Munsch
c88583d727
Fix printMultiProductSheet
making 2 sheets for the firs product item
2023-09-11 11:28:07 +02:00
Jérémy Munsch
6aab7d4eff
Fix product resource with comments 2023-09-11 11:23:42 +02:00
Jérémy Munsch
0681bf8f0a
Oci migration Products comments 2023-09-11 11:08:19 +02:00
Jérémy Munsch
923c3e0f6a
oci_migration fix wrong keywords associated with products 2023-09-11 09:57:57 +02:00
Jérémy Munsch
8e48fc5ea1
Fix product Filter
Fixes #39
Fixes #35
Fixes #36
2023-09-08 17:38:56 +02:00
Jérémy Munsch
615263f233
Refactor and fixes
Fixes #33
Fixes #34
2023-09-08 16:02:21 +02:00
Jérémy Munsch
16f4af41fd
Fix product-body division by ero
when product->weight is 0
2023-09-08 13:29:25 +02:00
Jérémy Munsch
d51f78a8db
Fix for ProductResource resource 2023-09-08 13:27:32 +02:00
Jérémy Munsch
25b2e42f14
Merge branch 'migration' into develop 2023-09-08 12:06:59 +02:00
Jérémy Munsch
b9f51a1a5d
Products form move tendance au dessus de commentaire 2023-09-06 18:29:35 +02:00
Jérémy Munsch
e381de5e9c
Closes #28
Fix oci migration picture on products
2023-09-06 18:19:22 +02:00
Jérémy Munsch
042e78583e
add image resizer 2023-09-06 18:19:22 +02:00
Jérémy Munsch
e6796029ec
oci_migrate set picture as nullable 2023-09-06 18:19:22 +02:00
Jérémy Munsch
39fc13dfe2
re set oci_migration elements 2023-09-06 18:19:21 +02:00
Jérémy Munsch
686bf307a2
Push down migrate_oci_table order 2023-09-06 18:19:21 +02:00
Jérémy Munsch
549f874abd
Set validation image as non required as it should be 2023-09-06 18:19:21 +02:00
Noah Lancien
17ac0aaa5c
Fixed Products and Brands factories 2023-09-06 18:19:21 +02:00
Noah Lancien
f5367815e1
Fixed product brand field in the form 2023-09-06 18:19:20 +02:00
Jérémy Munsch
0d1f135616
lint on product Controller 2023-09-06 18:19:20 +02:00
Jérémy Munsch
817ffa68fc
Product migration should be ok 2023-09-06 18:19:20 +02:00
Jérémy Munsch
a31512216c
Change Enum Components dans enum styore on products to what it should be (array on integers) 2023-09-06 18:19:20 +02:00
Jérémy Munsch
cc24e547d2
Fix SelectMultipleEnumComponent as ti should be (generate array of integers) 2023-09-06 18:19:20 +02:00
Noah Lancien
16fe3773a9
Added brand creation in product form, fixed missing * when required, cleaning 2023-09-06 18:19:19 +02:00
Jérémy Munsch
8ce9d5734b
Vue component add name on components 2023-09-06 18:19:19 +02:00
Jérémy Munsch
6de828bc31
Fix product old file delete 2023-09-06 18:19:19 +02:00
Jérémy Munsch
fe3eb7a955
laravel lang install 2023-09-06 18:19:18 +02:00
Jérémy Munsch
c0bce88e75
Overhaul pagination 2023-09-06 18:19:18 +02:00
Jérémy Munsch
61e900ee32
Fixes #27 ProductController fix filter code 2023-09-06 18:19:14 +02:00
Jérémy Munsch
f44d9d7849
upgrade vite config 2023-09-06 14:26:19 +02:00
Jérémy Munsch
ecaec2823f
color setup 2023-09-06 14:26:19 +02:00
Jérémy Munsch
22b598a11a
console output fix 2023-09-06 14:26:18 +02:00
Jérémy Munsch
d9c132ad25
Fix comp alleg 2023-09-06 14:26:18 +02:00
Jérémy Munsch
634dbe832e
Products and Brands Migration OK except for products assets
Waiting for
https://lab.s-web.fr/ldc-web/projets/startcoq/-/issues/28#note_9782
2023-09-06 14:26:18 +02:00
Jérémy Munsch
f539cdd99a
add drop temp table system 2023-09-06 14:23:18 +02:00
Jérémy Munsch
52dda4e346
Migration automatique de table START_C 2023-09-06 14:23:18 +02:00
Noah Lancien
8a3058e7bc Fixed some requests from the provided excel document 2023-09-05 15:28:55 +02:00
Vincent BRIDIER
97623ce240 Merge branch 'products' into 'develop'
add CRUD Products - #5

Closes #5, #12, and #7

See merge request ldc-web/projets/startcoq!6
2023-06-21 10:24:56 +00:00
Alexis Gousseau
7e8c4e7efd add CRUD Products - #5 2023-06-21 10:24:56 +00:00
Vincent BRIDIER
151e24d775 Merge branch 'seeders' into 'develop'
generate factories from models

Closes #4

See merge request ldc-web/projets/startcoq!5
2023-03-06 12:10:17 +00:00
Alexis Gousseau
254a14a3f8 generate factories from models 2023-02-28 16:38:05 +01:00
Vincent BRIDIER
b600a890aa Merge branch 'models' into 'develop'
add models generate from database

See merge request ldc-web/projets/startcoq!4
2023-02-23 15:22:44 +00:00
Alexis Gousseau
5d7c58f4cf generate models from database 2023-02-23 16:12:54 +01:00
Vincent BRIDIER
91b8732434 Merge branch 'Enums' into 'develop'
add Enums

See merge request ldc-web/projets/startcoq!3
2023-02-23 15:06:44 +00:00
Vincent BRIDIER
0cc60c62d4 Merge branch 'migrations' into 'develop'
Migrations

See merge request ldc-web/projets/startcoq!2
2023-02-23 15:05:14 +00:00
Alexis Gousseau
ae78c6d1fe add migrations generate from a database exported from workbench 2023-02-23 13:10:48 +01:00
Alexis Gousseau
95c9dd10d5 add enums 2023-02-23 10:26:11 +01:00
Vincent BRIDIER
f7f14c6283 Merge branch 'refonte' into 'develop'
base Laravel 10

See merge request ldc-web/projets/startcoq!1
2023-02-23 08:36:31 +00:00
Alexis Gousseau
d454c83fec remove sanctum package 2023-02-23 09:17:15 +01:00
Alexis Gousseau
c9d988e64b add home page + bootstrap custom + sass/ts files 2023-02-20 12:42:49 +01:00
Alexis Gousseau
00c809b8a0 init laravel 10 2023-02-20 10:01:53 +01:00
vincent.bridier
c3831006e0 update ext for issue templates 2023-02-15 14:35:01 +00:00
vincent.bridier
73412cd632 Add issue templates 2023-02-15 14:31:04 +00:00
272 changed files with 37858 additions and 0 deletions

21
.editorconfig Normal file
View file

@ -0,0 +1,21 @@
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
[*.vue]
indent_size = 2

58
.env.example Normal file
View file

@ -0,0 +1,58 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

55
.eslintrc.json Normal file
View file

@ -0,0 +1,55 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:vue/vue3-strongly-recommended",
"@vue/eslint-config-typescript"
],
"overrides": [
{
"files": ["*.ts", "*.vue"],
"rules": {
"no-undef": "off"
}
},
{
"files": ["*.vue"],
"rules": {
"indent": "off"
}
}
],
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["vue", "@typescript-eslint"],
"ignorePatterns": ["webpack.mix.js", "**/vendor/*.js", "**/vendor/**/*.js"],
"rules": {
"indent": ["error", 4, { "SwitchCase": 1 }],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double"],
"semi": ["error", "always"],
"vue/script-indent": ["error", 2, { "baseIndent": 0, "switchCase": 1 }],
"vue/one-component-per-file": "off"
},
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".vue", ".ts", ".d.ts"]
},
"alias": {
"extensions": [".vue", ".js", ".ts", ".scss", ".d.ts"],
"map": [
["~", "./resources/assets"],
["@", "./node_modules"]
]
}
}
}
}

11
.gitattributes vendored Normal file
View 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

20
.gitignore vendored Normal file
View file

@ -0,0 +1,20 @@
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.env.production
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
.phpunit.result.cache
/.fleet
/.idea
/.vscode
.stylelintcache

View file

@ -0,0 +1,6 @@
## Comportement constaté
## Comportement attendu
## Solution envisagée## Étapes pour la reproduction du bug
* step

View file

@ -0,0 +1,5 @@
## Présentation du besoin
## Implémentation
## Maquettes, documentations...

41
.stylelintrc.json Normal file
View file

@ -0,0 +1,41 @@
{
"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",
"**/vendor/*.less",
"**/vendor/**/*.less",
"**/vendor/*.vue",
"**/vendor/**/*.vue"
],
"customSyntax": "postcss-scss",
"rules": {
"indentation": 4,
"string-quotes": "double",
"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"
]
}
]
}
}

22
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,22 @@
{
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#90431a",
"activityBar.background": "#90431a",
"activityBar.foreground": "#e7e7e7",
"activityBar.inactiveForeground": "#e7e7e799",
"activityBarBadge.background": "#1da24b",
"activityBarBadge.foreground": "#e7e7e7",
"commandCenter.border": "#e7e7e799",
"sash.hoverBorder": "#90431a",
"statusBar.background": "#652f12",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#90431a",
"statusBarItem.remoteBackground": "#652f12",
"statusBarItem.remoteForeground": "#e7e7e7",
"titleBar.activeBackground": "#652f12",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#652f1299",
"titleBar.inactiveForeground": "#e7e7e799"
},
"peacock.remoteColor": "#652f12"
}

View file

@ -0,0 +1,9 @@
# StartCoQ
## Installation
```
cp .env.example .env
php artisan key:generate
php artisan storage:link
php artisan migrate [--seed]
```

31
app/Console/Kernel.php Normal file
View file

@ -0,0 +1,31 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule): void
{
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands(): void
{
$this->load(__DIR__ . '/Commands');
require base_path('routes/console.php');
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Enums\Ingredients;
use App\Traits\Enums\BaseEnum;
enum FamilyEnum : int
{
use BaseEnum;
case alcool = 1;
case flavor = 2;
case naturalCasings = 3;
case groceryStore = 4;
case spicesHerbs = 5;
case flourBreadcrumbs = 6;
case fruitsVegetables = 7;
case aromaticMixMarinade = 8;
case functionalMixSimpleIngredients = 9;
case meatProducts = 10;
case sauces = 11;
case dairyProducts = 12;
/**
* Custom added families definition
*/
private const LABELS = [
self::alcool->name => 'Alcool',
self::flavor->name => 'Arôme',
self::naturalCasings->name => 'Boyaux Naturels',
self::groceryStore->name => 'Épicerie',
self::spicesHerbs->name => 'Épices et aromates',
self::flourBreadcrumbs->name => 'Farine / Chapelure',
self::fruitsVegetables->name => 'Fruits et légumes',
self::aromaticMixMarinade->name => 'Mix aromatique / Marinade',
self::functionalMixSimpleIngredients->name => 'Mix fonctionnel / ingredients simples',
self::meatProducts->name => 'Produits carnés',
self::sauces->name => 'Sauces',
self::dairyProducts->name => 'Produits laitiers',
];
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Enums\Ingredients;
use App\Traits\Enums\BaseEnum;
enum FunctionEnum : int
{
use BaseEnum;
case dye = 1;
case antiOxidant = 2;
case starch = 3;
case curator = 4;
case freezingAgent = 5;
case sweetener = 6;
case stabilizer = 7;
case emulsifier = 8;
case thickener = 9;
case sugar = 10;
case salt = 11;
case fiber = 12;
case protein = 13;
/**
* Custom added functions definition
*/
private const LABELS = [
self::dye->name => 'Colorant',
self::antiOxidant => 'Anti-oxidant',
self::starch->name => 'Amidon',
self::curator->name => 'Conservateur',
self::freezingAgent->name => 'Gelifiant',
self::sweetener->name => 'Edulcorant',
self::stabilizer->name => 'Stabilisateur',
self::emulsifier->name => 'Emulsifiant',
self::thickener->name => 'Epaississant',
self::sugar->name => 'Sucre',
self::salt->name => 'Sel',
self::fiber->name => 'Fibre',
self::protein->name => 'Protéine',
];
}

View file

@ -0,0 +1,27 @@
<?php
namespace App\Enums\Ingredients;
use App\Traits\Enums\BaseEnum;
enum OriginEnum : int
{
use BaseEnum;
case magazine = 1;
case photo = 2;
case web = 3;
case mail = 4;
case salon = 5;
/**
* Custom added origins->name definition
*/
private const LABELS = [
self::magazine->name => 'Magazine',
self::photo->name => 'Photo de rayon',
self::web->name => 'Web',
self::mail->name => 'Fournisseur/Mail',
self::salon->name => 'Salon'
];
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Enums\Ingredients;
use App\Traits\Enums\BaseEnum;
enum PackagingEnum : int
{
use BaseEnum;
case fees = 1;
case pasteurizedSterilized = 2;
case frozen = 3;
case bagDehydrated = 4;
/**
* Custom added packagings definition
*/
private const LABELS = [
self::fees->name => 'Frais',
self::pasteurizedSterilized->name => 'Pasteurisé / Stérilisé',
self::frozen->name => 'Surgelé',
self::bagDehydrated->name => 'Sac / déshydraté',
];
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Enums;
use App\Traits\Enums\BaseEnum;
enum KeywordCategoryEnum : string
{
use BaseEnum;
case ingredientPleasure = "ingredient_pleasure";
case ingredientPracticality = "ingredient_practicality";
case ingredientHealth = "ingredient_health";
case ingredienEthics = "ingredient_ethics";
case productPleasure = "product_pleasure";
case productShape = "product_shape";
case productPracticality = "product_practicality";
case productHealth = "product_health";
case productEthics = "product_ethics";
case packagingPleasure = "packaging_pleasure";
case packagingPracticality = "packaging_practicality";
case packagingEthics = "packaging_ethics";
case packagingDesign = "packaging_design";
/**
* Custom added functions definition
*/
private const LABELS = [
self::ingredientPleasure->name => 'IngredientPleasure',
self::ingredientPracticality->name => 'IngredientPracticality',
self::ingredientHealth->name => 'IngredientHealth',
self::ingredienEthics->name => 'IngredientEthics',
self::productPleasure->name => 'ProductPleasure',
self::productShape->name => 'ProductShape',
self::productPracticality->name => 'ProductPracticality',
self::productHealth->name => 'ProductHealth',
self::productEthics->name => 'ProductEthics',
self::packagingPleasure->name => 'PackagingPleasure',
self::packagingPracticality->name => 'PackagingPracticality',
self::packagingEthics->name => 'PackagingEthics',
self::packagingDesign->name => 'PackagingDesign',
];
}

View file

@ -0,0 +1,37 @@
<?php
namespace App\Enums\Packaging;
use App\Traits\Enums\BaseEnum;
enum BenefitEnum : int
{
use BaseEnum;
case resistance = 1;
case recyclability = 2;
case presentation = 3;
case openingClosing = 4;
case toaster = 5;
case traditionalOven = 6;
case microwaveOven = 7;
case communication = 8;
case barbecue = 9;
case others = 10;
/**
* Custom added benefits definition
*/
private const LABELS = [
self::resistance->name => 'Résistance',
self::recyclability->name => 'Recyclabilité',
self::presentation->name => 'Présentation',
self::openingClosing->name => 'Ouverture/Fermeture',
self::toaster->name => 'Grille pain',
self::traditionalOven->name => 'Four Traditionnel',
self::microwaveOven->name => 'Four micro-onde',
self::communication->name => 'Communication',
self::barbecue->name => 'Barbecue',
self::others->name => 'Autres'
];
}

View file

@ -0,0 +1,35 @@
<?php
namespace App\Enums\Packaging;
use App\Traits\Enums\BaseEnum;
enum MaterialEnum : int
{
use BaseEnum;
case plactic = 1;
case paper = 2;
case wood = 3;
case aluminium = 4;
case inox = 5;
case glass = 6;
case ceramic = 7;
case materials = 8;
case others = 9;
/**
* Custom added materials definition
*/
private const LABELS = [
self::plactic->name => 'Plastique',
self::paper->name => 'Carton/Papier',
self::wood->name => 'Bois/Osier',
self::aluminium->name => 'Aluminium',
self::inox->name => 'Acier/Inox',
self::glass->name => 'Verre',
self::ceramic->name => 'Céramique/Terre cuite/Grès',
self::materials->name => 'Multi Matériaux',
self::others->name => 'Autres'
];
}

View file

@ -0,0 +1,27 @@
<?php
namespace App\Enums\Packaging;
use App\Traits\Enums\BaseEnum;
enum OriginEnum : int
{
use BaseEnum;
case magazine = 1;
case photo = 2;
case web = 3;
case mail = 4;
case salon = 5;
/**
* Custom added origins definition
*/
private const LABELS = [
self::magazine->name => 'Magazine',
self::photo->name => 'Photo de rayon',
self::web->name => 'Web',
self::mail->name => 'Fournisseur/Mail',
self::salon->name => 'Salon'
];
}

View file

@ -0,0 +1,37 @@
<?php
namespace App\Enums\Packaging;
use App\Traits\Enums\BaseEnum;
enum TypeEnum : int
{
use BaseEnum;
case box = 1;
case bag = 2;
case jar = 3;
case bottle = 4;
case film = 5;
case sleeves = 6;
case blister = 7;
case sheathCase = 8;
case tag = 9;
case others = 10;
/**
* Custom added types definition
*/
private const LABELS = [
self::box->name => 'Boîte/Barquette/Bol',
self::bag->name => 'Sac/Sachet',
self::jar->name => 'Pot',
self::bottle->name => 'Bouteille/Gobelet/Flacon',
self::film->name => 'Film',
self::sleeves->name => 'Sleeves',
self::blister->name => 'Tube/Blister/Feuille',
self::sheathCase->name => 'Étui fourreau',
self::tag->name => 'Étiquettes',
self::others->name => 'Autres'
];
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Enums\Packaging;
use App\Traits\Enums\BaseEnum;
enum WatchTypeEnum : int
{
use BaseEnum;
case product = 1;
case packaging = 2;
/**
* Custom added watch types definition
*/
private const LABELS = [
self::product->name => 'Produit Fini',
self::packaging->name => 'Emballage',
];
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Enums\Pagination;
use App\Traits\Enums\BaseEnum;
enum ItemsPerPaginationEnum: int
{
use BaseEnum;
case five = 5;
case twelve = 12;
case twentyfive = 25;
case fifty = 50;
case onehundred = 100;
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Enums\Process;
use App\Traits\Enums\BaseEnum;
enum BenefitEnum : int
{
use BaseEnum;
case rateGain = 1;
case capacityGain = 2;
case efficiencyGain = 3;
case laborGain = 4;
case packagingGain = 5;
case energyGain = 6;
case others = 7;
/**
* Custom added benefits definition
*/
private const LABELS = [
self::rateGain->name => 'Gain Cadence',
self::capacityGain->name => 'Gain capacité',
self::efficiencyGain->name => 'Gain rendement',
self::laborGain->name => 'Gain main d\'oeuvre',
self::packagingGain->name => 'Conso Emballage',
self::energyGain->name => 'Conso Energie',
self::others->name => 'Autres'
];
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Enums\Process;
use App\Traits\Enums\BaseEnum;
enum MeatPreparationEnum : int
{
use BaseEnum;
case cutting = 1;
case mixture = 2;
case grinding = 3;
case nibbling = 4;
case churning = 5;
case injection = 6;
case others = 7;
/**
* Custom added meat preparations definition
*/
private const LABELS = [
self::cutting->name => 'Cutterage',
self::mixture->name => 'Mélange',
self::grinding->name => 'Broyage',
self::nibbling->name => 'Grignotage',
self::churning->name => 'Barattage',
self::injection->name => 'Injection',
self::others->name => 'Autres'
];
}

View file

@ -0,0 +1,27 @@
<?php
namespace App\Enums\Process;
use App\Traits\Enums\BaseEnum;
enum OriginEnum : int
{
use BaseEnum;
case magazine = 1;
case photo = 2;
case web = 3;
case mail = 4;
case salon = 5;
/**
* Custom added origins definition
*/
private const LABELS = [
self::magazine->name => 'Magazine',
self::photo->name => 'Photo de rayon',
self::web->name => 'Web',
self::mail->name => 'Fournisseur/Mail',
self::salon->name => 'Salon'
];
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Enums\Process;
use App\Traits\Enums\BaseEnum;
enum PrimaryPackingEnum : int
{
use BaseEnum;
case sealing = 1;
case thermoforming = 2;
case verticalBagging = 3;
case horizontalBagging = 4;
case skin = 5;
case others = 6;
/**
* Custom added primary packagings definition
*/
private const LABELS = [
self::sealing->name => 'Operculage',
self::thermoforming->name => 'Thermoformage',
self::verticalBagging->name => 'Ensachage Vertical',
self::horizontalBagging->name => 'Ensachage Horizontal',
self::skin->name => 'Skin',
self::others->name => 'Autres'
];
}

View file

@ -0,0 +1,49 @@
<?php
namespace App\Enums\Process;
use App\Traits\Enums\BaseEnum;
enum TypeEnum : int
{
use BaseEnum;
case slaughter = 1;
case beef = 2;
case slicing = 3;
case cook = 4;
case freezing = 5;
case forming = 6;
case mainPackaging = 7;
case secondPackaging = 8;
case calibring = 9;
case inspection = 10;
case identification = 11;
case labeling = 12;
case conveyance = 13;
case logistics = 14;
case robotization = 15;
case others = 16;
/**
* Custom added types definition
*/
private const LABELS = [
self::slaughter->name => 'Abbattage/Découpe',
self::beef->name => 'Préparation de viande',
self::slicing->name => 'Tranchage/Émincage',
self::cook->name => 'Cuisson',
self::freezing->name => 'Refroidissement/Surgélation',
self::forming->name => 'Formage/Embossage/Portionnage',
self::mainPackaging->name => 'Conditionnement primaire',
self::secondPackaging->name => 'Conditionnement secondaire',
self::calibring->name => 'Pesage/Calibrage',
self::inspection->name => 'Contrôles/Inspection',
self::identification->name => 'Identification (Marqueurs/Jets d\'encres',
self::labeling->name => 'Étiquetage',
self::conveyance->name => 'Convoyage/Transitique',
self::logistics->name => 'Logistiques',
self::robotization->name => 'Robotisation',
self::others->name => 'Autres'
];
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Enums\Process;
use App\Traits\Enums\BaseEnum;
enum WatchTypeEnum : int
{
use BaseEnum;
case product = 1;
case packaging = 2;
case process = 3;
/**
* Custom added watch types definition
*/
private const LABELS = [
self::product->name => 'Produit Fini',
self::packaging->name => 'Emballage',
self::process->name => 'Process',
];
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Enums\Products;
use App\Traits\Enums\BaseEnum;
enum CertificationEnum: int
{
use BaseEnum;
case bioAB = 1;
case labelRouge = 2;
case certifiedCQ = 3;
case standard = 4;
case freeRange = 6;
case others = 5;
/**
* Custom added functions definition
*/
private const LABELS = [
self::bioAB->name => 'Bio AB ou Européen',
self::labelRouge->name => 'Label Rouge',
self::certifiedCQ->name => 'Certifié CQ',
self::standard->name => 'Standard',
self::freeRange->name => 'Free Range',
self::others->name => 'Autres',
];
}

View file

@ -0,0 +1,40 @@
<?php
namespace App\Enums\Products;
use App\Traits\Enums\BaseEnum;
enum ClaimEnum: int
{
use BaseEnum;
case productOfYear = 1;
case omega3 = 2;
case withoutOgm = 3;
case withoutGluten = 4;
case withoutAdditives = 5;
case withoutAntibiotics = 6;
case vegetarian = 7;
case lightweight = 8;
case halal = 9;
case highVitamins = 10;
case others = 99;
/**
* Custom added functions definition
*/
private const LABELS = [
self::productOfYear->name => 'Produit de l\'année',
self::omega3->name => 'Bleu Blanc Coeur, source d\'Omega 3',
self::withoutOgm->name => 'Sans OGM',
self::withoutGluten->name => 'Sans gluten',
self::withoutAdditives->name => 'Sans additif, sans colorant, sans conservateur,
100% simple, sans polyphosphate, sans sel nitrité',
self::withoutAntibiotics->name => 'Sans Antibiotique',
self::vegetarian->name => 'Vegan',
self::lightweight->name => 'Allégé',
self::halal->name => 'Halal, Casher',
self::highVitamins->name => 'Riche en protéines, fibres, calcium…',
self::others->name => 'Autres',
];
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Enums\Products;
use App\Traits\Enums\BaseEnum;
enum DistributionNetworkEnum: int
{
use BaseEnum;
case gms = 1;
case freezer = 2;
case restauration = 3;
case product = 4;
/**
* Custom added functions definition
*/
private const LABELS = [
self::gms->name => 'GMS / Hard discount / Commerce de détail',
self::freezer->name => 'Home service/ Freezer Center',
self::restauration->name => 'Restauration',
self::product->name => 'Produit pour industrie agro-alimentaire',
];
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Enums\Products;
use App\Traits\Enums\BaseEnum;
enum GeoAreaEnum: int
{
use BaseEnum;
case french = 1;
case european = 2;
case asia = 3;
case america = 4;
case africa = 5;
case oceania = 6;
/**
* Custom added functions definition
*/
private const LABELS = [
self::french->name => 'France',
self::european->name => 'Europe',
self::asia->name => 'Asie',
self::america->name => 'Amérique',
self::africa->name => 'Afrique',
self::oceania->name => 'Océanie',
];
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Enums\Products;
use App\Traits\Enums\BaseEnum;
enum OriginEnum: int
{
use BaseEnum;
case frenchPreparation = 1;
case frenchProduct = 2;
case igp = 3;
case regionalOrigins = 4;
case aoc = 5;
case ue = 6;
case others = 99;
/**
* Custom added functions definition
*/
private const LABELS = [
self::frenchPreparation->name => 'Né, élevé et préparé en France',
self::frenchProduct->name => 'Produit en France',
self::igp->name => 'IGP',
self::regionalOrigins->name => 'Origines régionales',
self::aoc->name => 'AOC',
self::ue->name => 'UE',
self::others->name => 'Autres',
];
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Enums\Products;
use App\Traits\Enums\BaseEnum;
enum RangeEnum: int
{
use BaseEnum;
case crus = 1;
case elaboratedRaw = 2;
case culinaryAids = 3;
case cooked = 4;
case smoked = 5;
case cookingSauce = 6;
case breaded = 7;
case rawMeat = 8;
case cookedMeats = 9;
case farcis = 10;
case plant = 12;
case frozenSpecific = 13;
case others = 11;
/**
* Custom added functions definition
*/
private const LABELS = [
self::crus->name => 'Crus',
self::elaboratedRaw->name => 'Élaborés crus',
self::culinaryAids->name => 'Aides culinaires',
self::cooked->name => 'Cuits',
self::smoked->name => 'Fumés, confits ou séchés',
self::cookingSauce->name => 'Cuisine en sauce',
self::breaded->name => 'Panés',
self::rawMeat->name => 'Charcuterie crue',
self::cookedMeats->name => 'Charcuterie cuite',
self::farcis->name => 'Farcis',
self::plant->name => 'Végétal',
self::frozenSpecific->name => 'Spécifique surgelé',
self::others->name => 'Autres',
];
}

View file

@ -0,0 +1,35 @@
<?php
namespace App\Enums\Products;
use App\Traits\Enums\BaseEnum;
enum RayEnum: int
{
use BaseEnum;
case butchery = 1;
case poultry = 2;
case freshlyPackaged = 3;
case roastery = 4;
case caterer = 5;
case creamery = 7;
case groceryStore = 8;
case frozen = 9;
case others = 10;
/**
* Custom added functions definition
*/
private const LABELS = [
self::butchery->name => 'Boucherie',
self::poultry->name => 'Volaille',
self::freshlyPackaged->name => 'Frais emballé',
self::roastery->name => 'Rôtisserie',
self::caterer->name => 'Traiteur',
self::creamery->name => 'Crémerie',
self::groceryStore->name => 'Épicerie',
self::frozen->name => 'Surgelé',
self::others->name => 'Autres',
];
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Enums\Products;
use App\Traits\Enums\BaseEnum;
enum SourceEnum: int
{
use BaseEnum;
// * Catalogue argumentaire
case catalogArgumentary = 7;
// * Magasin
case magazin = 1;
// * Presse
case press = 3;
// * Salon Grand Public
case showConsumer = 6;
// * Salon Clients
case showCustomer = 5;
// * Salon Professionnel
case showTrade = 4;
// * Web
case web = 2;
/**
* Custom added functions definition
*/
private const LABELS = [
self::catalogArgumentary->name => 'Catalogue Argumentaire',
self::magazin->name => 'Magasin',
self::press->name => 'Presse',
self::showConsumer->name => 'Salon Grand Public',
self::showCustomer->name => 'Salon Clients',
self::showTrade->name => 'Salon Professionnel',
self::web->name => 'Web',
];
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Enums\Promotions;
use App\Traits\Enums\BaseEnum;
enum AngleEnum : int
{
use BaseEnum;
case brand = 1;
case eco = 2;
case fun = 3;
/**
* Custom added functions definition
*/
private const LABELS = [
self::brand->name => 'Démarche de la marque',
self::eco->name => 'Eco',
self::fun->name => 'Fun',
];
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Enums\Promotions;
use App\Traits\Enums\BaseEnum;
enum DistributionNetworkEnum : int
{
use BaseEnum;
case gms = 1;
case hd = 2;
case rhd = 3;
case export = 4;
/**
* Custom added functions definition
*/
private const LABELS = [
self::gms->name => 'GMS',
self::hd->name => 'HD',
self::rhd->name => 'RHD',
self::export->name => 'Export',
];
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Enums\Promotions;
use App\Traits\Enums\BaseEnum;
enum EventEnum : int
{
use BaseEnum;
case birthday = 1;
case summer = 2;
case filmLicense = 3;
case christmas = 4;
case celebrityPartnership = 5;
case sport = 6;
/**
* Custom added functions definition
*/
private const LABELS = [
self::birthday->name => 'Anniversaire',
self::summer->name => 'Été',
self::filmLicense->name => 'Licence film / Dessin animé',
self::christmas->name => 'Noël',
self::celebrityPartnership->name => 'Partenariat célébrité',
self::sport->name => 'Sport',
];
}

View file

@ -0,0 +1,33 @@
<?php
namespace App\Enums\Promotions;
use App\Traits\Enums\BaseEnum;
enum LabelDistributionNetworkEnum : int
{
use BaseEnum;
case allDistributions = 1;
case lidle = 2;
case aldi = 3;
case erteco = 4;
case netto = 5;
case leaderPrice = 6;
case noLvl2 = 7;
case others = 8;
/**
* Custom added functions definition
*/
private const LABELS = [
self::allDistributions->name => 'Toutes les enseignes',
self::lidle->name => 'LIDL',
self::aldi->name => 'ALDI',
self::erteco->name => 'ERTECO',
self::netto->name => 'NETTO',
self::leaderPrice->name => 'LEADER PRICE',
self::noLvl2->name => 'Pas de niveau 2',
self::others->name => 'Autres',
];
}

View file

@ -0,0 +1,27 @@
<?php
namespace App\Enums\Promotions;
use App\Traits\Enums\BaseEnum;
enum SourceEnum : int
{
use BaseEnum;
case fieldTour = 1;
case headFeedback = 2;
case web = 3;
case press = 4;
case events = 5;
/**
* Custom added functions definition
*/
private const LABELS = [
self::fieldTour->name => 'Tournée terrain',
self::headFeedback->name => 'Remontée chef Secteur',
self::web->name => 'Pige web',
self::press->name => 'Pige presse',
self::events->name => 'Salon / Évenementiel',
];
}

View file

@ -0,0 +1,27 @@
<?php
namespace App\Enums\Promotions;
use App\Traits\Enums\BaseEnum;
enum TargetEnum : int
{
use BaseEnum;
case women = 1;
case man = 2;
case junior = 3;
case senior = 4;
case allTargets = 5;
/**
* Custom added functions definition
*/
private const LABELS = [
self::women->name => 'Femme',
self::man->name => 'Homme',
self::junior->name => 'Junior',
self::senior->name => 'Senior',
self::allTargets->name => 'Toutes cibles',
];
}

View file

@ -0,0 +1,39 @@
<?php
namespace App\Enums\Promotions;
use App\Traits\Enums\BaseEnum;
enum TypeEnum : int
{
use BaseEnum;
case animation = 1;
case reduction = 2;
case fidelity = 3;
case consoGames = 4;
case promotionalFormats = 5;
case flyer = 6;
case coBranding = 7;
case merchandising = 8;
case goodies = 9;
case promotionalMaterials = 10;
case ephemeralProducts = 11;
/**
* Custom added functions definition
*/
private const LABELS = [
self::animation->name => 'Animation',
self::reduction->name => 'Réduction',
self::fidelity->name => 'Fidélité',
self::consoGames->name => 'Jeux Conso',
self::promotionalFormats->name => 'Formats promo',
self::flyer->name => 'OP Trade / Prospectus',
self::coBranding->name => 'Co-branding',
self::merchandising->name => 'PLV / Merchandising',
self::goodies->name => 'Primes / Goodies',
self::promotionalMaterials->name => 'Argumentaires Promo',
self::ephemeralProducts->name => 'Produits éphèmères',
];
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Enums\Surveys;
use App\Traits\Enums\BaseEnum;
enum CustomerEnum : int
{
use BaseEnum;
case francoscopie = 1;
case packaging = 2;
case innovation = 3;
case snacking = 4;
case vegetarian = 5;
case christmas = 6;
case barbecue = 7;
case sustainableDevelopment = 8;
case labelRouge = 9;
case bio = 10;
case certified = 11;
case bbc = 12;
case others = 13;
/**
* Custom added functions definition
*/
private const LABELS = [
self::francoscopie->name => 'Francoscopie',
self::packaging->name => 'Emaballages',
self::innovation->name => 'Innovation',
self::snacking->name => 'Snacking',
self::vegetarian->name => 'Vegan',
self::christmas->name => 'Noel',
self::barbecue->name => 'Barbecue',
self::sustainableDevelopment->name => 'Développement durable',
self::labelRouge->name => 'Label Rouge',
self::bio->name => 'BIO',
self::certified->name => 'Certifié',
self::bbc->name => 'BBC',
self::others->name => 'Autres',
];
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Enums\Surveys;
use App\Traits\Enums\BaseEnum;
enum DistributionNetworkEnum : int
{
use BaseEnum;
case gms = 1;
case freezer = 2;
case restauration = 3;
case product = 4;
/**
* Custom added functions definition
*/
private const LABELS = [
self::gms->name => 'GMS / Hard discount / Commerce de détail',
self::freezer->name => 'Home service/ Freezer Center',
self::restauration->name => 'Restauration',
self::product->name => 'Produit pour industrie agro-alimentaire',
];
}

View file

@ -0,0 +1,45 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of exception types with their corresponding custom log levels.
*
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/
protected $levels = [];
/**
* A list of the exception types that are not reported.
*
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [];
/**
* A list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*
* @return void
*/
public function register(): void
{
$this->reportable(function (Throwable $e) {
});
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Http\Requests\Brands\StoreBrandRequest;
use App\Models\Brand;
use Illuminate\Support\Facades\DB;
class BrandController extends Controller
{
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Brands\StoreBrandRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function store(StoreBrandRequest $request)
{
return DB::transaction(function () use ($request) {
$brand = new Brand();
$brand->fill($request->validated());
if ($brand->saveOrFail()) {
return \response()->json(['status' => true, 'brand' => $brand]);
}
return \response()->json(['status' => false, 'message' => __('La marque n\'a pas pu être créée')]);
});
}
}

View file

@ -0,0 +1,152 @@
<?php
namespace App\Http\Controllers;
use App\Enums\Pagination\ItemsPerPaginationEnum;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
class Controller extends BaseController
{
use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests;
/**
* Build Search query on specified fields
* splitting search into words or using whole sentence
*
* @param \Illuminate\Database\Eloquent\Builder $query The eloquent query builder.
* @param string $search The query string.
* @param callable $searchQuery The query string.
* @param string ...$fields Either a simple array or a string
* in case of a string the two fields
* would be concatenate to make the search.
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function searchQuery(
Builder $query,
string $search,
callable $searchQuery = null,
string ...$fields
): Builder {
if (count($fields)) {
// Search using words.
$words = explode(' ', $search);
$query->where(function ($query) use ($searchQuery, $words, $search, $fields) {
foreach ($words as $word) {
if (!strlen($word)) {
continue;
}
$this->searchOnFields($query, $word, $fields);
} //end foreach
// Search whole using whole sentence.
$this->searchOnFields($query, $search, $fields);
if ($searchQuery) {
$searchQuery($query);
}
});
} //end if
return $query;
}
/**
* Build query search on all fields
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $search
* @param array $fields
* @return \Illuminate\Database\Eloquent\Builder
*/
private function searchOnFields(Builder $query, string $search, array $fields): Builder
{
$table = $query->getModel()->getTable();
foreach ($fields as $f) {
$f = str_replace('?', '\?', $f);
if (is_array($f)) {
$query->orWhereRaw(
sprintf("UPPER(CONCAT(%s)) LIKE UPPER(?)", collect($f)->map(function ($q) use ($table) {
return "`$table`.`$q`";
})->implode(', \' \', ')),
['%' . filter_var($search, FILTER_SANITIZE_STRING) . '%']
);
} else {
$query->orWhereRaw(
"UPPER(`$table`.`$f`) LIKE UPPER(?)",
['%' . filter_var($search, FILTER_SANITIZE_STRING) . '%']
);
}
}
return $query;
}
/**
* Sort columns with a query
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param array $ignore
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function sortQuery(Builder $query, array $ignore = []): Builder
{
$rName = request()->route()->getName();
$table = $query->getModel()->getTable();
if (request()->sort_col) {
Session::put("$rName.sort_col", request()->sort_col);
Session::put("$rName.sorted", true);
}
if (request()->sort_way) {
Session::put("$rName.sort_way", request()->sort_way);
Session::put("$rName.sorted", true);
}
if (request()->rst) {
Session::remove("$rName.sorted");
Session::remove("$rName.sort_col");
Session::remove("$rName.sort_way");
}
if (request()->sort_col and request()->sort_way and Schema::hasColumn($table, request()->sort_col)) {
if (
!isset($ignore) or
(isset($ignore) and !in_array(request()->sort_col, $ignore))
) {
$query = $query->orderBy(\sprintf("$table.%s", request()->sort_col), request()->sort_way);
}
} elseif (Schema::hasColumn($table, 'order')) {
$query = $query->orderBy('order', 'ASC');
Session::put("$rName.sort_col", 'order');
Session::put("$rName.sort_way", 'asc');
}
return $query;
}
/**
* Customize pagination with cache or config (default).
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \App\Enums\Pagination\ItemsPerPaginationEnum|null $pagination
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
protected function paginate(Builder $query, ItemsPerPaginationEnum $pagination = null): LengthAwarePaginator
{
$currentRoutePath = Str::slug(request()->route()->getName());
$cacheKey = "pagination.{$currentRoutePath}";
$pagination = $pagination ?? ItemsPerPaginationEnum::twelve->value;
$pagination = ToolboxHelper::getValidatedPaginate(
Cache::get($cacheKey, $pagination)
);
if (Cache::get($cacheKey) !== $pagination) {
Cache::put($cacheKey, $pagination);
}
$query = $query->paginate($pagination);
return $query;
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
class DashboardController extends Controller
{
/**
* Show products list.
*
* @return \Illuminate\Contracts\View\View
*/
public function show(): \Illuminate\Contracts\View\View
{
return view('pages.dashboard');
}
}

View file

@ -0,0 +1,451 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Http\Requests\Products\IndexFilterRequest;
use App\Http\Requests\Products\StoreProductRequest;
use App\Http\Requests\Products\UpdateProductRequest;
use App\Jobs\ExportProduct;
use App\Models\Brand;
use App\Models\Keyword;
use App\Models\Product;
use App\Traits\Controllers\HasDocuments;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class ProductController extends Controller
{
use HasDocuments;
/**
* Display a listing of the resource.
*
* @param \App\Http\Requests\Products\IndexFilterRequest $request
* @return \Illuminate\Contracts\View\View
*/
public function index(IndexFilterRequest $request)
{
/** @var \Illuminate\Database\Eloquent\Builder $brands */
$brandsQuery = Brand::query();
/** @var \Illuminate\Database\Eloquent\Builder $productsQuery */
$productsQuery = Product::query()->orderBy('created_at', 'desc');
$this->requestFilters($request, $productsQuery);
$this->sortQuery($productsQuery);
$products = $this->paginate($productsQuery);
/** @var array $printMultiProduct Array of id */
$printMultiProduct = session('printMultiProduct') ?? [];
$brands = $brandsQuery->get();
$keywords = Keyword::query()->where('keyword_cat', 'LIKE', 'product_%')->get();
return view('pages.products.index', [
'products' => $products,
'brands' => $brands,
'keywords' => $keywords,
'printMultiProduct' => $printMultiProduct
]);
}
/**
* Show the form for creating a new resource.
*
* @param App\Models\Product $productModel
* @param Illuminate\Support\Collection $commentModel
* @return \Illuminate\Contracts\View\View
* phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
*/
public function create(Product $productModel, Collection $commentModel): \Illuminate\Contracts\View\View
{
//phpcs:enable
$brands = Brand::all();
$productsFair = Product::query()->pluck('fair');
$productsShop = Product::query()->pluck('shop');
$productsCatalog = Product::query()->pluck('catalogue');
$keywords = Keyword::query()->where('keyword_cat', 'product_pleasure')
->orWhere('keyword_cat', 'product_shape')
->orWhere('keyword_cat', 'product_practicality')
->orWhere('keyword_cat', 'product_health')
->orWhere('keyword_cat', 'product_ethics')
->get();
return view('pages.products.create', compact(
'productModel',
'brands',
'productsFair',
'productsShop',
'productsCatalog',
'keywords',
'commentModel'
));
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Products\StoreProductRequest $request
* @return \Illuminate\Http\Response
*/
public function store(StoreProductRequest $request)
{
return DB::transaction(function () use ($request) {
$product = new Product();
$product->fill($request->validated());
$this->storeDocuments($request, $product);
if ($product->saveOrFail()) {
$this->updateCommentsModel($request, $product);
$product->productKeywords()->sync(collect($request->productKeywords)->pluck('id'));
return redirect()
->route('products.edit', $product)
->with('success', trans(
':model a été créé',
['model' => Str::singular(trans('models.classes.products'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été créé',
['model' => Str::singular(trans('models.classes.products'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Product $product
* @return \Illuminate\Http\Response
*/
public function edit(Product $product)
{
$brands = Brand::all();
$productsFair = Product::query()->pluck('fair');
$productsShop = Product::query()->pluck('shop');
$productsCatalog = Product::query()->pluck('catalogue');
$commentModel = $product->productComments;
$keywords = Keyword::query()->where('keyword_cat', 'product_pleasure')
->orWhere('keyword_cat', 'product_shape')
->orWhere('keyword_cat', 'product_practicality')
->orWhere('keyword_cat', 'product_health')
->orWhere('keyword_cat', 'product_ethics')
->get();
return view('pages.products.edit', [
'productModel' => $product,
'brands' => $brands,
'productsFair' => $productsFair,
'productsShop' => $productsShop,
'productsCatalog' => $productsCatalog,
'commentModel' => $commentModel,
'keywords' => $keywords,
]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Products\UpdateProductRequest $request
* @param \App\Models\Product $product
* @return \Illuminate\Http\Response
*/
public function update(UpdateProductRequest $request, Product $product)
{
return DB::transaction(function () use ($request, $product) {
$product->fill($request->validated());
$this->storeDocuments($request, $product);
$this->updateCommentsModel($request, $product);
$product->productKeywords()->sync(collect($request->productKeywords)->pluck('id'));
if ($product->saveOrFail()) {
return redirect()
->route('products.edit', $product)
->with('success', trans(
':model a été mis à jour',
['model' => Str::singular(trans('models.classes.products'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été mis à jour',
['model' => Str::singular(trans('models.classes.products'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Product $product
* @return \Illuminate\Http\Response
*/
public function destroy(Product $product)
{
$this->deleteCommentsModel($product);
if ($product->deleteOrFail()) {
return redirect()
->route('products.index')
->with('warning', trans(
':model a bien été supprimé',
['model' => Str::singular(trans('models.classes.products'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été supprimé',
['model' => Str::singular(trans('models.classes.products'))]
));
}
/**
* Update comments model.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\Product $product
* @return void
*/
public function updateCommentsModel(Request $request, Product $product)
{
$this->deleteCommentsModel($product);
if ($request->evaluation) {
foreach ($request->evaluation as $key => $value) {
$product->productComments()->create([
'evaluation' => $request->evaluation[$key],
'aspect' => $request->aspect[$key] ?? null,
'taste' => $request->taste[$key] ?? null,
'odor' => $request->odor[$key] ?? null,
'texture' => $request->texture[$key] ?? null,
]);
}
}
}
/**
* Delete comments model.
*
* @param \App\Models\Product $product
* @return \Illuminate\Http\Response
*/
public function deleteCommentsModel(Product $product)
{
foreach ($product->productComments as $comment) {
if (!$comment->deleteOrFail()) {
return redirect()
->back()
->with('error', trans(
':model n\'a pas été supprimé',
['model' => Str::singular(trans('models.classes.products'))]
));
}
}
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Product $product
* @return \Illuminate\Http\Response
*/
public function duplicate(Product $product)
{
$duplicatedProduct = $product->replicate();
foreach ($product->productComments as $comment) {
$duplicatedComments[] = $comment->replicate();
}
return $this->create($duplicatedProduct, collect($duplicatedComments ?? null));
}
/**
* Return a boolean if the gencode is already used or not.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function checkGencode(Request $request)
{
if (
isset($request->GENCOD) &&
$request->GENCOD !== null &&
ctype_digit($request->GENCOD) &&
strlen($request->GENCOD) > 0
) {
if (
isset($request->ID) &&
$request->ID !== null &&
intval($request->GENCOD) === Product::where('id', $request->ID)->firstOrFail()->gencod
) {
$result = null;
} else {
$result = in_array(intval($request->GENCOD), Product::pluck('gencod')->toArray()) ?
Product::orderBy('created_at', 'desc')->where('gencod', intval($request->GENCOD))->first() : null;
}
} else {
$result = null;
}
return response()->json($result);
}
/**
* Print product commercial sheet.
*
* @param \Illuminate\Http\Request $request
* @param integer $id
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function printProductSheet(Request $request = null, int $id = null)
{
$productId = $id ?? intval($request->id);
$product = Product::where('id', $productId)->firstOrFail();
$title = trans('Fiche produit : :name - :appname', [
'name' => $product->name,
'appname' => \config('app.name', 'Startcoq')
]);
$filename = trans('fiche-product-:slug-:date', [
'slug' => Str::slug($product->name),
'date' => \now()->format('Y-m-d')
]);
return Product::generateProductPdf($product, $title)->stream("{$filename}.pdf");
}
/**
* Print multi product commercial sheet
*
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function printMultiProductSheet()
{
// FIXME: This should not use session.
/** @var array printMultiProduct */
$printMultiProduct = session('printMultiProduct') ?? [];
asort($printMultiProduct);
if (count($printMultiProduct)) {
$title = trans('Fiches produits - :appname', [
'appname' => \config('app.name', 'Startcoq')
]);
Product::generateMultipleProductPdf($printMultiProduct, $title)->stream();
} else {
return redirect()
->back()
->with('warning', trans('Veuillez sélectionner des produits'));
}
}
/**
* Print product commercial sheet
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function printMultiProductUpdate(Request $request)
{
/** @var integer Id of the actual product */
$productId = intval($request->ID);
/** @var array Array of id products */
$printMultiProduct = session('printMultiProduct') ?? [];
if (in_array($productId, $printMultiProduct)) {
array_splice($printMultiProduct, array_search($productId, $printMultiProduct), 1);
$result = false;
} else {
$printMultiProduct[] = $productId;
$result = true;
}
session(['printMultiProduct' => $printMultiProduct]);
return $result;
}
/**
* Remove the selection,
* destroy the specified session.
*
* @return \Illuminate\Http\RedirectResponse
*/
public function printMultiProductDestroy()
{
session()->forget('printMultiProduct');
return redirect()
->back()
->with('success', trans('Sélection des :model supprimée', ['model' => trans('models.classes.products')]));
}
/**
* Download a .zip file.
*
* @param \Illuminate\Http\Request $request
* @return void
*/
public function download(Request $request)
{
\dispatch(new ExportProduct($request->id))->onConnection('database');
}
/**
* Applies filters from products.index
*
* @param \App\Http\Requests\Products\IndexFilterRequest $request
* @param \Illuminate\Database\Eloquent\Builder $productsQuery
* @return \Illuminate\Contracts\View\View
* @phpcs:disable Generic.Metrics.NestingLevel.TooHigh
*/
private function requestFilters(
IndexFilterRequest $request,
Builder $productsQuery
): \Illuminate\Database\Eloquent\Builder {
//phpcs:enable
foreach ($request->validated() as $parameterName => $queryParameter) {
$productsQuery->when(
!empty($queryParameter),
function (Builder $q) use ($queryParameter, $parameterName) {
switch ($parameterName) {
// * String type parameters (Approx search)
case 'name':
case 'shop':
case 'fair':
case 'catalogue':
case 'specified_claim':
$q->where($parameterName, 'LIKE', "%{$queryParameter}%");
break;
// * Number type parameters (Approx search)
case 'gencode':
$q->where($parameterName, \intval($queryParameter));
break;
// * Date type parameters
case 'date_before':
$q->whereDate('created_at', '<=', $queryParameter);
break;
case 'date_after':
$q->whereDate('created_at', '>=', $queryParameter);
break;
// * Enums Array
case 'claims':
case 'origins':
$q->whereJsonContains($parameterName, \collect($queryParameter)
->map(fn ($enumValue) => \intval($enumValue)));
break;
// * RelationShips parameters
case 'keywords':
$q->whereHas('productKeywords', function (Builder $subQ) use ($queryParameter) {
$subQ->whereIn('keywords.id', $queryParameter);
});
break;
default:
$q->where($parameterName, $queryParameter);
} //end switch
}
);
} //end foreach
return $productsQuery;
}
}

62
app/Http/Kernel.php Normal file
View file

@ -0,0 +1,62 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
\App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\SecureHeaders::class
],
];
/**
* The application's middleware aliases.
*
* Aliases may be used to conveniently assign middleware to routes and groups.
*
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string|null
*/
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : route('login');
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
];
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [
];
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request):(\Symfony\Component\HttpFoundation\Response) $next
* @param string ...$guards
* @return \Symfony\Component\HttpFoundation\Response
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/
public function handle(Request $request, \Closure $next, string ...$guards): Response
{
// phpcs:enable
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}

View file

@ -0,0 +1,105 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\View;
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 array
*/
private $unwantedHeaderList = [
'X-Powered-By',
'Server',
];
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @return \Symfony\Component\HttpFoundation\Response
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/
public function handle(Request $request, Closure $next): Response
{
// phpcs:enable
View::share('nonce', $nonce = Vite::useCspNonce());
// * For VueJS dev tool to execute
$jsDev = config('app.debug') ? "'unsafe-eval' 'unsafe-inline'" : "'nonce-{$nonce}' 'strict-dynamic'";
$scheme = config('app.env') === 'local' ? 'http' : 'https';
$reportUri = Route::has('cspReportUri') ?
\sprintf(' report-uri %s ;', \parse_url(route('cspReportUri'), \PHP_URL_PATH)) : '';
$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;');
$cspPolicies = [
"default-src 'none';",
"manifest-src 'self';",
// * CKEDITOR Specific https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/csp.html
"connect-src 'self' blob:;",
"img-src * data: blob:;",
'frame-src ' . implode(' ', [
'https://www.google.com/recaptcha/',
'https://recaptcha.google.com/recaptcha/',
'https://www.google.com/maps/',
'blob:'
]) . ';',
"object-src 'self' data:;",
"font-src 'self' data: https://fonts.gstatic.com;",
// ! Using CKEditor will must have unsafe-inline style
// phpcs:ignore Generic.Files.LineLength.TooLong
"style-src 'self' 'unsafe-inline' " . \implode(' ', [
'https://cdnjs.cloudflare.com',
// * File Manager
'https://fonts.googleapis.com',
'https://cdn.jsdelivr.net',
'https://use.fontawesome.com',
'https://fonts.googleapis.com'
]) . ';',
// ! If you dont use ckeditor use this
// "style-src 'self' 'nonce-$nonce';",
// phpcs:ignore Generic.Files.LineLength.TooLong
"script-src 'self' {$jsDev} " . \implode(' ', [
'https: blob:',
'https://www.googletagmanager.com',
'https://cdnjs.cloudflare.com',
"$scheme://www.google-analytics.com",
]) . ';'
];
$response->headers->set(
'Content-Security-Policy',
implode(' ', $cspPolicies) . $reportUri
);
$response->headers->set('Feature-Policy', 'fullscreen \'self\';');
return $response;
}
/**
* Remove unwanted headers
*
* @param array $headerList
* @return void
*/
private function removeUnwantedHeaders(array $headerList)
{
foreach ($headerList as $header) {
header_remove($header);
}
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
*/
public function hosts(): array
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var integer
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*
* 'fbclid',
* 'utm_campaign',
* 'utm_content',
* 'utm_medium',
* 'utm_source',
* 'utm_term',
*/
protected $except = [];
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
];
}

View file

@ -0,0 +1,42 @@
<?php
namespace App\Http\Requests\Brands;
use Illuminate\Foundation\Http\FormRequest;
class StoreBrandRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'name' => 'required|string|min:3|max:255',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes(): array
{
return [
'name' => trans('nom de la marque'),
];
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace App\Http\Requests\Products;
use App\Enums\Products\CertificationEnum;
use App\Enums\Products\ClaimEnum;
use App\Enums\Products\DistributionNetworkEnum;
use App\Enums\Products\GeoAreaEnum;
use App\Enums\Products\OriginEnum;
use App\Enums\Products\RangeEnum;
use App\Enums\Products\RayEnum;
use App\Enums\Products\SourceEnum;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
class IndexFilterRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
return [
'name' => 'nullable|string',
'brand_id' => 'nullable|exists:brands,id',
'gencod' => 'nullable|string',
'source' => ['nullable', new Enum(SourceEnum::class)],
'shop' => 'nullable|string',
'fair' => 'nullable|string',
'catalogue' => 'nullable|string',
'claims' => 'nullable|array',
'claims.*' => ['required', new Enum(ClaimEnum::class)],
'specified_claim' => 'nullable|string',
'origins' => 'nullable|array',
'origins.*' => ['required', new Enum(OriginEnum::class)],
'certification' => ['nullable', new Enum(CertificationEnum::class)],
'distribution_network' => ['nullable', new Enum(DistributionNetworkEnum::class)],
'ray' => ['nullable', new Enum(RayEnum::class)],
'product_range' => ['nullable', new Enum(RangeEnum::class)],
'geo_area' => ['nullable', new Enum(GeoAreaEnum::class)],
'keywords' => 'nullable|array',
'keywords.*' => 'required|exists:keywords,id',
'date_before' => 'nullable|date:Y-m-d',
'date_after' => 'nullable|date:Y-m-d',
];
}
}

View file

@ -0,0 +1,160 @@
<?php
namespace App\Http\Requests\Products;
use App\Enums\Products\CertificationEnum;
use App\Enums\Products\ClaimEnum;
use App\Enums\Products\DistributionNetworkEnum;
use App\Enums\Products\GeoAreaEnum;
use App\Enums\Products\OriginEnum;
use App\Enums\Products\RangeEnum;
use App\Enums\Products\RayEnum;
use App\Enums\Products\SourceEnum;
use App\Lib\Helpers\ToolboxHelper;
use App\Traits\Requests\HasDocumentAndPictureList;
use App\Traits\Requests\HasPicture;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\ValidationException;
class StoreProductRequest extends FormRequest
{
use HasPicture;
use HasDocumentAndPictureList;
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return true;
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation()
{
$this->merge([
'id' => $this->route('product')->id ?? null,
'is_tasted' => $this->is_tasted ? true : false,
'origins' => $this->origins ? $this->origins : [],
'claims' => $this->claims ? $this->claims : [],
'picture_alt' => $this->name,
'picture_title' => $this->name,
'certification' => $this->certification == 0 ? null : $this->certification,
]);
$this->mergePicture('picture');
$this->mergeDocuments('docs');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
$rules = [
'name' => 'required|string|min:3|max:255',
'brand_id' => 'required|exists:brands,id',
'source' => [new Enum(SourceEnum::class)],
'fair' => 'nullable|string|min:3|max:255',
'shop' => 'nullable|string|min:3|max:255',
'catalogue' => 'nullable|string|min:3|max:255',
'gencod' => 'nullable|numeric|min:0',
'picture_alt' => 'required|min:3|max:255',
'picture_title' => 'required|min:3|max:255',
'url' => [
'nullable', 'url', function ($attribute, string $value, $fail) {
try {
ToolboxHelper::assertNoInternalBoUrls($attribute, $value);
} catch (ValidationException $e) {
$fail(\trans('Impossible d\'utiliser une url du back-office.'));
}
}
],
'certification' => ['nullable', new Enum(CertificationEnum::class)],
'origins' => 'present|array',
'origins.*' => [new Enum(OriginEnum::class)],
'claims' => 'present|array',
'claims.*' => [new Enum(ClaimEnum::class)],
'specified_claim' => 'string|min:3|max:255|required_if:claims,' . ClaimEnum::others->value,
'distribution_network' => ['required', new Enum(DistributionNetworkEnum::class)],
'ray' => ['required', new Enum(RayEnum::class)],
'product_range' => ['required', new Enum(RangeEnum::class)],
'geo_area' => ['required', new Enum(GeoAreaEnum::class)],
'weight' => 'nullable|integer',
'pvc' => 'nullable|decimal:0,2',
'comment' => 'nullable|string|min:15',
'is_tasted' => 'required|boolean',
'tasters_number' => 'nullable|integer',
'evaluation' => 'nullable|array',
'evaluation.*' => 'sometimes|string|min:3|max:255',
'aspect' => 'nullable|array',
'aspect.*' => 'nullable|string|min:3|max:255',
'taste' => 'nullable|array',
'taste.*' => 'nullable|string|min:3|max:255',
'odor' => 'nullable|array',
'odor.*' => 'nullable|string|min:3|max:255',
'texture' => 'nullable|array',
'texture.*' => 'nullable|string|min:3|max:255',
];
return \array_merge(
$rules,
$this->pictureRules('picture', false, 1200, 900, 1200, 900),
$this->documentsRules('docs'),
);
}
/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes(): array
{
return [
'name' => trans('nom du produit'),
'brand_id' => trans('marque du produit'),
'source' => trans('source du produit'),
'fair' => trans('salon du produit'),
'shop' => trans('magazin du produit'),
'catalogue' => trans('catalogue du produit'),
'gencod' => trans('gencod du produit'),
'picture_alt' => trans('attribut alt du produit'),
'picture_title' => trans('attribut titre du produit'),
'docs' => trans('documents du produit'),
'url' => trans('lien du produit'),
'certification' => trans('certification'),
'origins' => trans('origines du produit'),
'origins.*' => trans('origine du produit'),
'claims' => trans('allégations du produit'),
'claims.*' => trans('allégation du produit'),
'specified_claim' => trans('allégation spécifique du produit'),
'distribution_network' => trans('réseau de distribution du produit'),
'ray' => trans('rayon du produit'),
'product_range' => trans('famille du produit'),
'geo_area' => trans('zone géographique du produit'),
'weight' => trans('poids du produit'),
'pvc' => trans('pvc de l\'uvc du produit'),
'comment' => trans('commentaire du produit'),
'is_tasted' => trans('produit dégusté ?'),
'tasters_number' => trans('nombre de dégustateurs du produit'),
'evaluation' => trans('évaluations du produit (partie commentaire)'),
'evaluation.*' => trans('évaluation du produit (partie commentaire)'),
'aspect' => trans('aspects du produit (partie commentaire)'),
'aspect.*' => trans('aspect du produit (partie commentaire)'),
'taste' => trans('goûts du produit (partie commentaire)'),
'taste.*' => trans('goût du produit (partie commentaire)'),
'odor' => trans('odeurs du produit (partie commentaire)'),
'odor.*' => trans('odeur du produit (partie commentaire)'),
'texture' => trans('textures du produit (partie commentaire)'),
'texture.*' => trans('texture du produit (partie commentaire)'),
];
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace App\Http\Requests\Products;
class UpdateProductRequest extends StoreProductRequest
{
}

View file

@ -0,0 +1,117 @@
<?php
namespace App\Http\Resources;
use App\Enums\Products\ClaimEnum;
use App\Enums\Products\OriginEnum;
use App\Models\Keyword;
use App\Models\Product;
use App\Models\ProductComment;
use Illuminate\Http\Resources\Json\JsonResource;
class ProductResource extends JsonResource
{
/** @var \App\Models\Product */
private $product;
/** @var array */
private static $skeleton = [
'id' => null,
'user' => null,
'brand' => null,
'name' => null,
'seen_at' => null,
'source' => null,
'fair' => null,
'shop' => null,
'catalogue' => null,
'gencod' => null,
'weight' => null,
'pvc' => null,
'picture' => null,
'picture_alt' => null,
'picture_title' => null,
'certification' => null,
'origins' => null,
'claims' => null,
'specified_claim' => null,
'distribution_network' => null,
'ray' => null,
'product_range' => null,
'geo_area' => null,
'freeComment' => null,
'productComments' => null,
'productKeywords' => null,
];
/**
* ProductResource
*
* @param \App\Models\Product $product
* @return void
*/
public function __construct(Product $product)
{
$this->product = $product;
}
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request|null $request
* @return array
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
*/
public function toArray($request = null)
{
return array_intersect_key(
[
'id' => $this->product->id,
'user' => (object)$this->product->user->toArray(),
'brand' => (object)$this->product->brand->toArray(),
'name' => $this->product->name,
'seen_at' => $this->product->seen_at->isoFormat('DD\/MM\/YYYY'),
'source' => $this->product->source->label(),
'fair' => $this->product->fair,
'shop' => $this->product->shop,
'catalogue' => $this->product->catalogue,
'gencod' => $this->product->gencod,
'weight' => $this->product->weight,
'pvc' => $this->product->pvc,
'picture' => $this->product->picture,
'picture_alt' => $this->product->picture_alt,
'picture_title' => $this->product->picture_title,
'certification' => \optional($this->product->certification)->label(),
'origins' => $this->product->origins_enum->map(fn (OriginEnum $enum) => $enum->label())->implode(', '),
'claims' => $this->product->claims_enum->map(fn (ClaimEnum $enum) => $enum->label())->implode(', '),
'specified_claim' => $this->product->specified_claim,
'distribution_network' => $this->product->distribution_network->label(),
'ray' => $this->product->ray->label(),
'product_range' => $this->product->product_range->label(),
'geo_area' => $this->product->geo_area->label(),
'freeComment' => $this->product->comment,
// FIXME: Use dedicaced resources for relations.
'productComments' => $this->product->productComments->map(function (ProductComment $comment) {
return (object)\array_merge($comment->toArray(), [
'created_at' => $comment->created_at->isoFormat('DD\/MM\/YYYY'),
'updated_at' => $comment->updated_at->isoFormat('DD\/MM\/YYYY'),
]);
}),
'productKeywords' => $this->product->productKeywords
->map(fn (Keyword $keyword) => $keyword->label)
->implode(', '),
],
$this->getSkeleton()
);
}
/**
* Returns the resource skeletton
*
* @return array
*/
public static function getSkeleton(): array
{
return self::$skeleton;
}
}

View file

@ -0,0 +1,81 @@
<?php
namespace App\Jobs;
use App\Models\Product;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class ExportProduct implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* Execute the job.
*
* @param integer $productId
* @return void
* @throws \Error If file storage config is wrong.
* @throws \RuntimeException If the storage disk export is not local file system.
*/
public function __construct(int $productId)
{
/** @var \App\Models\Product $product */
$product = Product::query()->where('id', intval($productId))->firstOrFail();
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$exportDisk = Storage::disk('export');
if (!($exportDisk instanceof \Illuminate\Filesystem\FilesystemAdapter)) {
throw new \Error('Storage disk export has to be local file system');
}
if (!$exportDisk->directoryExists('export-product')) {
$exportDisk->createDirectory('export-product');
}
// * Create zip
$zip = new \ZipArchive();
$fileName = date('d-m-Y') . '-' . Str::uuid() . '.zip';
if (!$zip->open($exportDisk->path("export-product/{$fileName}"), \ZipArchive::CREATE) === true) {
throw new \RuntimeException("The ZIP file cannot be created.");
}
$dirName = Str::slug(
Carbon::parse($product->updated_at)->format('y-m-d') . '-' .
$product->user->lastname . '-' . $product->user->id
);
if (!$zip->addEmptyDir($dirName)) {
throw new \RuntimeException("The folder cannot be created inside the ZIP.");
}
// * Add files
$pdf = Product::generateProductPdf($product, "test");
$exportDisk->put('pdf/' . $product->id . '.pdf', $pdf->output());
$zip->addFile($exportDisk->path('pdf/' . $product->id . '.pdf'), $dirName . "/" . $product->name . ".pdf");
$zip->addFile($product->picture, $dirName . "/photo." . pathinfo($product->picture, PATHINFO_EXTENSION));
foreach ($product->docs as $doc) {
$zip->addFile($doc, $dirName . "/document." . pathinfo($doc, PATHINFO_EXTENSION));
}
// * Save zip
$zip->close();
if (!$exportDisk->exists("export-product/{$fileName}")) {
throw new \RuntimeException("The ZIP file 'export-product/{$fileName}' has not been created.");
}
Product::downloadPrivateExportFile("export-product/{$fileName}");
}
}

View file

@ -0,0 +1,425 @@
<?php
namespace App\Lib\Helpers;
use Illuminate\Database\Eloquent\Model;
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 md5 file
* 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.
*
* @author Munsch Jérémy <jeremy.munsch@ldc.fr>
*/
class FileStorageHelper
{
/**
* Destroy the file associate with the attribute.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $attribute
* @return void
*/
public static function removeFile(Model $model, string $attribute): void
{
$path = \ltrim(\urldecode($model->{$attribute}), '/');
if (\strpos($path, 'storage') !== 0) {
return;
}
$path = ToolboxHelper::mbReplace('storage/', '', $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).
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $attribute
* @param boolean $onlyIfUpdated
* @return void
*/
public static function removeOldFile(Model $model, string $attribute, bool $onlyIfUpdated = true): void
{
// * Ignore If removal has to be done only if the field has changed and it actually hasnt.
if ($onlyIfUpdated and $model->{$attribute} === $model->getOriginal($attribute)) {
return;
}
$originalPath = \ltrim(\urldecode($model->getOriginal($attribute)), '/');
if (\strpos($originalPath, 'storage') !== 0) {
return;
}
$originalPath = ToolboxHelper::mbReplace('storage/', '', $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 \Illuminate\Database\Eloquent\Model $model The model that will hold the picture.
* @param \Illuminate\Http\UploadedFile|\SplFileInfo|string $file The file, will just return itself is string.
* @param boolean $slugify Force filename slugification.
* @param string|null $filename This string may be used to store the file.
* @return string
* @throws \RuntimeException If file copy fails.
*/
public static function storeFile(Model $model, $file, bool $slugify = false, string $filename = null): string
{
$tableName = $model->getTable();
if (\is_object($file) and $file instanceof \Illuminate\Http\UploadedFile) {
/** @var \Illuminate\Http\UploadedFile $file */
$filename = $filename ?: $file->getClientOriginalName();
$filename = $slugify ? self::slugifyFileName($filename) : $filename;
$newPath = self::prepareMoveFile($tableName, $filename);
$newPath = str_replace(storage_path('app'), '', $newPath);
$file->storePubliclyAs(
pathinfo($newPath, \PATHINFO_DIRNAME),
pathinfo($newPath, \PATHINFO_BASENAME)
);
return 'storage' . str_replace('/public', '', $newPath);
}
if (\is_object($file) and $file instanceof \SplFileInfo) {
/** @var \SplFileInfo $file */
$filename = $filename ?: $file->getBaseName();
$filename = $slugify ? self::slugifyFileName($filename) : $filename;
$newPath = self::prepareMoveFile($tableName, $filename);
if (!File::copy($file->getRealPath(), $newPath)) {
throw new \RuntimeException(sprintf(
'Failed to copy file %s to %s',
$file->getRealPath(),
$newPath
));
}
return \ltrim(str_replace(\storage_path('app/public'), '/storage', $newPath), '/');
}
return strval($file);
}
/**
* 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 \Illuminate\Database\Eloquent\Model $model The model that will hold the picture.
* @param string $htmlString The html string to parse and modify files.
* @param string|null $oldHtmlString The old html string with alreadyParsed
* files using storeModelFilesFromHtml.
* @return string Html with files pathes replaced using storage strategy.
*/
public static function storeModelFilesFromHtml(
Model $model,
string $htmlString,
string $oldHtmlString = null
): string {
\collect(self::onlyUploadedFilesWithFileManager(self::findFilesInHtml($htmlString)))
->each(function (string $path) use ($model, &$htmlString) {
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$disk = Storage::disk('uploads');
$fileInfo = new \SplFileInfo($disk->path(
ToolboxHelper::mbReplace('/uploads/', '', \urldecode($path))
));
$htmlString = ToolboxHelper::mbReplace(
$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.
* @return void
*/
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.
* @return void
*/
public static function removeOldFiles(array $oldFiles, array $newFiles): void
{
$oldFiles = \collect(self::onlyUploadedFilesWithFileStorageHelper($oldFiles));
$filesToKeep = \collect(self::onlyUploadedFilesWithFileStorageHelper($newFiles));
$filesToRemove = $oldFiles->diff($filesToKeep);
$filesToRemove->each(function (string $publicStorageFilePath) {
$path = ToolboxHelper::mbReplace('storage/', '', \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.
* @return void
*/
public static function removeModelFilesOfHtml(string $htmlString): void
{
$filesToRemove = \collect(self::onlyUploadedFilesWithFileStorageHelper(self::findFilesInHtml($htmlString)));
$filesToRemove->each(function (string $publicStorageFilePath) {
$path = ToolboxHelper::mbReplace('storage/', '', \ltrim(\urldecode($publicStorageFilePath), '/'));
if (Storage::disk('public')->exists($path)) {
Storage::disk('public')->delete($path);
}
});
}
/**
* Find all occurence of files in html
* @param string $html
* @return array
*/
public static function findFilesInHtml(string $html): array
{
preg_match_all('/href="(?P<href>.*)"|src="(?P<src>.*)"|url="(?P<url>.*)"/Um', $html, $m, PREG_SET_ORDER, 0);
return collect($m)->pluck('href')->filter()
->concat(collect($m)->pluck('url')->filter())
->concat(collect($m)->pluck('src')->filter())
->all();
}
/**
* Filter array to get only uploaded files that exists
* on disk 'public' (using helper storage strategy)
*
* @param array $urls
* @return array
*/
public static function onlyUploadedFilesWithFileStorageHelper(array $urls): array
{
return \collect($urls)->unique()
// * Filter to get only /uploads and using app url or empty
->filter(function (string $url) {
$path = \parse_url($url, \PHP_URL_PATH);
return \strpos(\ltrim($path, '/'), 'storage/modelfiles') === 0;
})->filter(function (string $url) {
$path = \parse_url($url, \PHP_URL_PATH);
return Storage::disk('public')
->exists(ToolboxHelper::mbReplace('storage/', '', \ltrim(\urldecode($path), '/')));
})->all();
}
/**
* Filter array to get only uploaded files that exists
* on disk 'uploads'
*
* @param array $urls
* @return array
*/
private static function onlyUploadedFilesWithFileManager(array $urls): array
{
return \collect($urls)->unique()
// * Filter to get only /uploads and using app url or empty
->filter(function (string $url) {
$host = \parse_url($url, \PHP_URL_HOST);
$path = \parse_url($url, \PHP_URL_PATH);
return ($host === \parse_url(config('app.url'), PHP_URL_HOST) or \is_null($host)) and
\strpos(\ltrim($path, '/'), 'uploads') === 0;
})->filter(function (string $url) {
$path = \parse_url($url, \PHP_URL_PATH);
return Storage::disk('uploads')
->exists(ToolboxHelper::mbReplace('uploads/', '', \ltrim(\urldecode($path), '/')));
})->all();
}
/**
* Prepare storage path and return
* the file full path
*
* @param string $tableName Model table name.
* @param string $filename The basename for the file.
* @return string
*/
private static function prepareMoveFile(string $tableName, string $filename): string
{
// * Security filter + beautifier
$filename = self::sanitizeFileName($filename);
// * Security filter
$filename = \filter_var($filename, \FILTER_SANITIZE_STRING);
// * Slugify filename
$ext = \pathinfo($filename, \PATHINFO_EXTENSION);
$filename = Str::slug(\pathinfo($filename, \PATHINFO_FILENAME)) .
(\strlen($ext) ? ".{$ext}" : '');
$folderPath = self::getStoragePath($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/md5 first 2 letters
*
* @param string $table
* @param string $filename
* @return string
*/
private static function getStoragePath(string $table, string $filename): string
{
$filenameHash = \md5($filename);
$pathFolder = \storage_path(sprintf(
'app/public/modelfiles/%s/%s/%s/%s/%s',
$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
*
* @param string $folderPath
* @param string $filename
* @return string
* @throws \RuntimeException 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 new \RuntimeException("Failed to has a unique filename for {$folderPath}/{$filename}");
}
$filename = pathinfo($filename, \PATHINFO_FILENAME) .
\uniqid('-') . '.' .
pathinfo($filename, \PATHINFO_EXTENSION);
}
return $filename;
}
/**
* Slugify the filename
*
* @param string $filename
* @return string
*/
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 boolean $beautify Beautify the filename.
* @return string The sanitized file name.
*/
public static function sanitizeFileName(string $filename, bool $beautify = true): string
{
// * Sanitize filename
// phpcs:disable Generic.Files.LineLength.TooLong
$filename = preg_replace(
'~
[<>:"/\\\|?*]| # file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
[\x00-\x1F]| # control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
[\x7F\xA0\xAD]| # non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN
[#\[\]@!$&\'()+,;=]| # URI reserved https://tools.ietf.org/html/rfc3986#section-2.2
[{}^\~`] # URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt
~x',
'-',
$filename
);
// phpcs:enable
// * Avoids '.' '..' or '.hiddenFiles' .
$filename = Str::of($filename)->ltrim('.-');
// * Optional beautification
if ($beautify) {
$filename = static::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 ? $encoding : null;
return \mb_strcut(
$filename,
0,
255 - (!empty($extension) ? strlen($extension) + 1 : 0),
$encoding
) . (!empty($extension) ? ".$extension" : '');
}
/**
* Beautify a filename
*
* @param string $filename
* @return string
*/
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 = preg_replace([
// * "file--.--.-.--name.zip" becomes "file.name.zip"
'/-*\.-*/',
// * "file...name..zip" becomes "file.name.zip"
'/\.{2,}/'
], '.', $filename);
// Lowercase for windows/unix interoperability http://support.microsoft.com/kb/100625 .
$filename = mb_strtolower($filename, mb_detect_encoding($filename));
// * ".file-name.-" becomes "file-name"
$filename = trim($filename, '.-');
return $filename;
}
}

View file

@ -0,0 +1,248 @@
<?php
namespace App\Lib\Helpers;
use App\Enums\Pagination\ItemsPerPaginationEnum;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ToolboxHelper
{
/**
* Cut string words
*
* @param string $string
* @param integer $max
* @param string $hyphen
* @return string
*/
public static function cutWords(string $string, int $max = 20, string $hyphen = '[.]')
{
$originalString = $string;
$tok = strtok($string, ' ');
$string = '';
while ($tok !== false && mb_strlen($string) < $max) {
if (
mb_strlen($string) + mb_strlen($tok) <= $max
) {
$string .= $tok . ' ';
} else {
break;
}
$tok = strtok(' ');
}
return trim($string) . (strlen(trim($string)) !== strlen(trim($originalString)) ? $hyphen : '');
}
/**
* 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..)
*
* @param string $input
* @return string
*/
public static function stripCkEditorSchemesOnMediaInputs(string $input): string
{
$input = \preg_replace('/(?<=<oembed url=")(https:\/\/)(?=.*?">)/m', '', $input);
return $input;
}
/**
* Assert That no Back Office url is present on txt
*
* @param string $field
* @param string $text
* @return string
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
*/
public static function assertNoInternalBoUrls(string $field, string $text): string
{
$routes = Route::getRoutes();
// * Url replace and email replace.
$text = preg_replace_callback(
// phpcs:ignore Generic.Files.LineLength.TooLong
'/(?P<url>(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*))/',
function ($matches) use ($routes, $field) {
$match = collect($matches)->first() ?? '';
$url = $match;
try {
$request = Request::create($url);
// If a route is match then it do not throws exception.
if (
\parse_url(config('app.url'), \PHP_URL_HOST) === \parse_url($url, \PHP_URL_HOST) &&
trim($routes->match($request)->getPrefix() ?? '', '/') === 'bo'
) {
// Removes the matched url.
throw ValidationException::withMessages([
$field => ':attribute : Il est interdit de coller une url du back office dans ce champ'
]);
}
return $match;
} catch (NotFoundHttpException $e) {
return $match;
}
},
$text
);
return $text;
}
/**
* Assert That only external url is present on txt
*
* @param string $field
* @param string $text
* @return string
* @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();
// * Url replace and email replace.
$text = preg_replace_callback(
// phpcs:ignore Generic.Files.LineLength.TooLong
'/(?P<url>(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*))/',
function ($matches) use ($routes, $field) {
$match = collect($matches)->first() ?? '';
$url = $match;
try {
$request = Request::create($url);
// If a route is match then it do not throws exception.
if (
\parse_url(config('app.url'), \PHP_URL_HOST) === \parse_url($url, \PHP_URL_HOST) &&
$routes->match($request)
) {
// Removes the matched url.
throw ValidationException::withMessages([
$field => sprintf(
':attribute : Il est interdit de coller une url de %s dans ce champ',
config('app.name')
)
]);
}
return $match;
} catch (NotFoundHttpException $e) {
return $match;
}
},
$text
);
return $text;
}
/**
* Mutltibyte string replace
* @param string|array $search
* @param string|array $replace
* @param mixed $subject
* @param integer $count
* @return mixed
*/
public static function mbReplace(string|array $search, string|array $replace, mixed $subject, int &$count = 0)
{
if (!is_array($search) && is_array($replace)) {
return false;
}
if (is_array($subject)) {
// Call mb_replace for each single string in $subject .
foreach ($subject as &$string) {
$string = &self::mbReplace($search, $replace, $string, $count);
}
} elseif (is_array($search)) {
if (!is_array($replace)) {
foreach ($search as &$string) {
$subject = self::mbReplace($string, $replace, $subject, $count);
}
} else {
$n = max(count($search), count($replace));
while ($n--) {
$subject = self::mbReplace(current($search), current($replace), $subject, $count);
next($search);
next($replace);
}
}
} else {
$parts = mb_split(preg_quote($search), $subject);
$count = count($parts) - 1;
$subject = implode($replace, $parts);
} //end if
return $subject;
}
/**
* Asserts that the field is unique
*
* @param string $table
* @param string $field
* @param mixed $value
* @param integer|null $id
* @param callable|null $query
* @return void
* @throws \Illuminate\Validation\ValidationException If the field is not unique.
*/
public static function assertFieldIsUnique(
string $table,
string $field,
mixed $value,
?int $id = null,
?callable $query = null
) {
if (!$query) {
Validator::make([$field => $value], [
$field => Rule::unique($table, $field)->ignore($id),
], [
$field . '.unique' => trans('La valeur :value pour le champ :attribute est déjà utilisé', [
'attribute' => $field,
'value' => $value
])
])->validate();
return;
}
Validator::make([$field => $value], [
$field => Rule::unique($table)->where($query)->ignore($id),
], [
$field . '.unique' => trans('La valeur pour le champ :attribute est déjà utilisé', [
'attribute' => $field
])
])->validate();
}
/**
* Check that two arrays have the same content (1 level only)
*
* @param array $array1
* @param array $array2
* @return boolean
*/
public static function arrayHaveSameContent(array $array1, array $array2): bool
{
return (array_diff($array1, $array2) == [] && array_diff($array2, $array1) == []);
}
/**
* Get validated pagination from request
*
* @param integer $default
* @return integer
*/
public static function getValidatedPaginate(int $default): int
{
try {
return \intval(Validator::make(
['pagination' => \request()->get('pagination')],
['pagination' => new Enum(ItemsPerPaginationEnum::class)]
)->validated()['pagination']);
} catch (ValidationException $e) {
return $default;
}
}
}

55
app/Models/Brand.php Normal file
View file

@ -0,0 +1,55 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property integer $id
* @property string $name
* @property string $filter
* @property string $created_at
* @property string $updated_at
* @property Promo[] $promos
* @property Promo[] $promo
* @property Product[] $products
*/
class Brand extends Model
{
use HasFactory;
/**
* @var array
*/
protected $fillable = [
'name',
'filter',
'created_at',
'updated_at'
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function promos()
{
return $this->belongsToMany('App\Models\Promo', 'promo_brands');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function promo()
{
return $this->hasMany('App\Models\Promo');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function products()
{
return $this->hasMany('App\Models\Product');
}
}

80
app/Models/Ingredient.php Normal file
View file

@ -0,0 +1,80 @@
<?php
namespace App\Models;
use App\Enums\Ingredients\OriginEnum;
use App\Enums\Ingredients\FamilyEnum;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property integer $id
* @property integer $user_id
* @property string $name
* @property string $supplier
* @property enum $origin
* @property boolean $is_sample
* @property enum $family
* @property string $packing
* @property string $function
* @property string $observation
* @property string $picture
* @property string $url
* @property string $documents
* @property string $fair
* @property string $created_at
* @property string $updated_at
* @property string $deleted_at
* @property User $user
*/
class Ingredient extends Model
{
use HasFactory;
/**
* @var array
*/
protected $fillable = [
'user_id',
'name',
'supplier',
'origin',
'is_sample',
'family',
'packing',
'function',
'observation',
'picture',
'url',
'documents',
'fair',
'created_at',
'updated_at',
'deleted_at'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'origin' => OriginEnum::class,
'is_sample' => 'boolean',
'family' => FamilyEnum::class,
'packing' => 'json',
'function' => 'json',
'documents' => 'json',
'created_at' => 'datetime:Y-m-d',
'updated_at' => 'datetime:Y-m-d',
'deleted_at' => 'datetime:Y-m-d',
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('App\Models\User');
}
}

59
app/Models/Keyword.php Normal file
View file

@ -0,0 +1,59 @@
<?php
namespace App\Models;
use App\Enums\KeywordCategoryEnum;
use Illuminate\Database\Eloquent\Model;
/**
* @property integer $id
* @property string $label
* @property \App\Enums\KeywordCategoryEnum $keyword_cat
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*
* * RELATIONSHIPS
* @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\Product> $products
* @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\Keyword> $keywordables
*/
class Keyword extends Model
{
/**
* @var array
*/
protected $fillable = [
'label',
'keyword_cat',
'created_at',
'updated_at'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'keyword_cat' => KeywordCategoryEnum::class
];
// * RELATIONSHIPS
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function keywordables()
{
return $this->hasMany(Keywordable::class);
}
/**
* MorphToMany products relation.
*
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
*/
public function products()
{
return $this->morphToMany(Product::class, 'keywordable');
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property integer $id
* @property integer $keyword_id
* @property integer $keywordable_id
* @property string $keywordable_type
* @property string $created_at
* @property string $updated_at
* @property Keyword $keyword
*/
class Keywordable extends Model
{
use HasFactory;
/**
* @var array
*/
protected $fillable = [
'keyword_id',
'keywordable_id',
'keywordable_type',
'created_at',
'updated_at'
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function keyword()
{
return $this->belongsTo('App\Models\Keyword');
}
}

86
app/Models/Packaging.php Normal file
View file

@ -0,0 +1,86 @@
<?php
namespace App\Models;
use App\Enums\Packaging\OriginEnum;
use App\Enums\Packaging\WatchTypeEnum;
use App\Enums\Packaging\TypeEnum;
use App\Enums\Packaging\MaterialEnum;
use App\Enums\Packaging\BenefitEnum;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property integer $id
* @property integer $user_id
* @property boolean $watch_type
* @property string $supplier
* @property string $name
* @property string $product_description
* @property boolean $origin
* @property boolean $is_sample
* @property boolean $type_packaging
* @property boolean $material
* @property boolean $benefit
* @property string $observation
* @property string $picture
* @property string $url
* @property string $documents
* @property string $fair
* @property string $created_at
* @property string $updated_at
* @property string $deleted_at
* @property User $user
*/
class Packaging extends Model
{
use HasFactory;
/**
* @var array
*/
protected $fillable = [
'user_id',
'watch_type',
'supplier',
'name',
'product_description',
'origin',
'is_sample',
'type_packaging',
'material',
'benefit',
'observation',
'picture',
'url',
'documents',
'fair',
'created_at',
'updated_at',
'deleted_at'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'watch_type' => WatchTypeEnum::class,
'origin' => OriginEnum::class,
'is_sample' => 'boolean',
'type_packaging' => TypeEnum::class,
'material' => MaterialEnum::class,
'benefit' => BenefitEnum::class,
'picture' => 'json',
'documents' => 'json',
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('App\Models\User');
}
}

93
app/Models/Process.php Normal file
View file

@ -0,0 +1,93 @@
<?php
namespace App\Models;
use App\Enums\Process\WatchTypeEnum;
use App\Enums\Process\OriginEnum;
use App\Enums\Process\BenefitEnum;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property integer $id
* @property integer $user_id
* @property boolean $watch_type
* @property string $supplier
* @property string $name
* @property string $description
* @property boolean $origin
* @property boolean $is_test_machine
* @property boolean $is_quotation
* @property string $quotation_document
* @property boolean $profit
* @property string $observation
* @property string $picture
* @property string $documents
* @property string $url
* @property string $created_at
* @property string $updated_at
* @property string $deleted_at
* @property ProcessHasProcessType[] $processHasProcessTypes
* @property User $user
*/
class Process extends Model
{
use HasFactory;
/**
* @var array
*/
protected $fillable = [
'user_id',
'watch_type',
'supplier',
'name',
'description',
'origin',
'is_test_machine',
'is_quotation',
'quotation_document',
'profit',
'observation',
'picture',
'documents',
'url',
'created_at',
'updated_at',
'deleted_at'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'watch_type' => WatchTypeEnum::class,
'origin' => OriginEnum::class,
'is_test_machine' => 'boolean',
'is_quotation' => 'boolean',
'profit' => BenefitEnum::class,
'picture' => 'json',
'documents' => 'json',
'created_at' => 'datetime:Y-m-d',
'updated_at' => 'datetime:Y-m-d',
'deleted_at' => 'datetime:Y-m-d',
];
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function processHasProcessTypes()
{
return $this->hasMany('App\Models\ProcessHasProcessType', 'process_id');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('App\Models\User');
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property integer $process_id
* @property integer $process_type_id
* @property string $value_enum
* @property ProcessType $processType
* @property Process $process
*/
class ProcessHasProcessType extends Model
{
use HasFactory;
/**
* @var array
*/
protected $fillable = [
'process_id',
'process_type_id',
'value_enum'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'value_enum' => 'json'
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function processType()
{
return $this->belongsTo('App\Models\ProcessType');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function process()
{
return $this->belongsTo('App\Models\Process');
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property integer $id
* @property string $name
* @property string $created_at
* @property string $updated_at
* @property ProcessHasProcessType[] $processHasProcessTypes
*/
class ProcessType extends Model
{
/**
* @var array
*/
protected $fillable = [
'name',
'created_at',
'updated_at'
];
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function processHasProcessTypes()
{
return $this->hasMany('App\Models\ProcessHasProcessType', 'process_type_id');
}
}

452
app/Models/Product.php Normal file
View file

@ -0,0 +1,452 @@
<?php
namespace App\Models;
use App\Enums\Products\CertificationEnum;
use App\Enums\Products\ClaimEnum;
use App\Enums\Products\DistributionNetworkEnum;
use App\Enums\Products\GeoAreaEnum;
use App\Enums\Products\OriginEnum;
use App\Enums\Products\RangeEnum;
use App\Enums\Products\RayEnum;
use App\Enums\Products\SourceEnum;
use App\Http\Resources\ProductResource;
use App\Lib\Helpers\FileStorageHelper;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Mccarlosen\LaravelMpdf\LaravelMpdf;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* FIXME: using migration, do not set nullable props without a real need.
*
* @property integer $id
* @property integer $user_id
* @property integer $brand_id
* @property string $name
* @property \Carbon\Carbon $seen_at
* @property \App\Enums\Products\SourceEnum $source
* @property string|null $fair
* @property string|null $shop
* @property string|null $catalogue
* @property string|null $gencod
* @property string|null $picture
* @property string $picture_alt
* @property string $picture_title
* @property array|null $docs
* @property string|null $url
* @property \App\Enums\Products\CertificationEnum|null $certification
* @property array<integer>|null $origins
* @property \Illuminate\Support\Collection<\App\Enums\Products\OriginEnum> $origins_enum
* @property array<integer>|null $claims
* @property \Illuminate\Support\Collection<\App\Enums\Products\OriginEnum> $claims_enum
* @property string|null $specified_claim
* @property \App\Enums\Products\DistributionNetworkEnum $distribution_network
* @property \App\Enums\Products\RayEnum $ray
* @property \App\Enums\Products\RangeEnum $product_range
* @property \App\Enums\Products\GeoAreaEnum $geo area
* @property integer|null $weight
* @property integer|null $pvc
* @property string|null $comment
* @property boolean $is_tasted
* @property integer|null $tasters_number
* @property \Carbon\Carbon|null $created_at
* @property \Carbon\Carbon|null $updated_at
* @property \Carbon\Carbon|null $deleted_at
*
* * RELATIONSHIPS
* @property-read \App\Models\Brand $brand
* @property-read \App\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\ProductComment> $productComments
* @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\Keyword> $productKeywords
*/
class Product extends Model
{
use HasFactory;
/**
* @var array
*/
protected $fillable = [
'user_id',
'brand_id',
'name',
// ? FIXME: ?why is there brand here, column is not in DB.
'brand',
'seen_at',
'source',
'fair',
'shop',
'catalogue',
'gencod',
'picture',
'picture_alt',
'picture_title',
'docs',
'url',
'certification',
'origins',
'claims',
'specified_claim',
'distribution_network',
'ray',
'product_range',
'geo_area',
'weight',
'pvc',
'comment',
'is_tasted',
'tasters_number',
'created_at',
'updated_at',
'deleted_at'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'source' => SourceEnum::class,
'docs' => 'json',
'certification' => CertificationEnum::class,
'origins' => 'json',
'claims' => 'json',
'distribution_network' => DistributionNetworkEnum::class,
'ray' => RayEnum::class,
'product_range' => RangeEnum::class,
'geo_area' => GeoAreaEnum::class,
'is_tasted' => 'boolean',
'seen_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
// * EVENTS
/**
* The "booted" method of the model.
*
* @return void
*/
protected static function booted(): void
{
static::creating(function (self $model) {
self::assertFieldIsUnique('name', $model->name);
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
self::updateSource($model);
// FIXME: lier le produit à l'utilisateur connecté lors de l'ajout du système de connexion.
$model->user_id = 1;
});
static::updating(function (self $model) {
self::deleteOldDocuments($model, true);
$model->picture = FileStorageHelper::storeFile($model, $model->picture, true);
self::updateSource($model);
self::updateSpecifiedClaim($model);
});
static::updated(function (self $model) {
FileStorageHelper::removeOldFile($model, 'picture');
});
static::deleted(function (self $model) {
self::deleteOldDocuments($model);
FileStorageHelper::removeFile($model, 'picture');
});
}
// * METHODS
/**
* Asserts using validation that the field is unique.
*
* @param string $field
* @param string $value
* @param integer|null $id
* @return void
* @throws \Illuminate\Validation\ValidationException If field already exists.
*/
private static function assertFieldIsUnique(string $field, string $value, ?int $id = null)
{
$table = (new self())->getTable();
ToolboxHelper::assertFieldIsUnique($table, $field, $value, $id);
}
/**
* Update data specified claim.
*
* @param \App\Models\Product $model
* @return void
*/
private static function updateSpecifiedClaim(Product $model)
{
$check = false;
foreach ($model->claims as $claimValue) {
if (!$check) {
$check = (intval($claimValue) === \App\Enums\Products\ClaimEnum::others->value) ? true : false;
}
}
$model->specified_claim = ($check) ? $model->specified_claim : null;
}
/**
* Update data source.
*
* @param self $model
* @return void
*/
private static function updateSource(self $model)
{
if (
$model->source === \App\Enums\Products\SourceEnum::showConsumer ||
$model->source === \App\Enums\Products\SourceEnum::showCustomer ||
$model->source === \App\Enums\Products\SourceEnum::showTrade
) {
$model->shop = null;
$model->catalogue = null;
} elseif ($model->source === \App\Enums\Products\SourceEnum::magazin) {
$model->fair = null;
$model->catalogue = null;
} elseif ($model->source === \App\Enums\Products\SourceEnum::catalogArgumentary) {
$model->fair = null;
$model->shop = null;
} else {
$model->fair = null;
$model->shop = null;
$model->catalogue = null;
};
}
/**
* Delete old documents.
*
* @param self $model
* @param boolean $onlyIfUpdated
* @return void
*/
public static function deleteOldDocuments(self $model, bool $onlyIfUpdated = false)
{
$original = \collect($model->getOriginal('docs'))->map(fn ($document) => strval($document));
$new = \collect($model->docs)->map(fn ($document) => strval($document))->flip();
foreach ($original as $document) {
// * Ignore if document is to keep.
if ($onlyIfUpdated and $new->has($document)) {
continue;
}
$originalPath = \ltrim(\urldecode($document), '/');
if (\strpos($originalPath, 'storage') !== 0) {
continue;
}
$originalPath = ToolboxHelper::mbReplace('storage/', '', $originalPath);
if (Storage::disk('public')->exists($originalPath)) {
Storage::disk('public')->delete($originalPath);
}
}
}
/**
* Generate product sheet PDF.
*
* @param self $model
* @param string $title
* @return \Mccarlosen\LaravelMpdf\LaravelMpdf
*/
public static function generateProductPdf(self $model, string $title): LaravelMpdf
{
/** @var \Mccarlosen\LaravelMpdf\LaravelMpdf */
$mpdf = app('laravel-mpdf');
/** @var \Mccarlosen\LaravelMpdf\LaravelMpdf */
$pdf = $mpdf->loadView(
'documents.product',
// * Dirty convert to object.
[
'title' => $title,
'product' => \json_decode(\json_encode(new ProductResource($model)))
],
[],
[
'mode' => 'utf-8',
'format' => 'A4',
'default_font_size' => '12',
'default_font' => 'sans-serif',
'mirrorMargins' => true,
'margin_left' => 10,
'margin_right' => 10,
'margin_top' => 10,
'margin_bottom' => 10,
'margin_header' => 10,
'margin_footer' => 10,
'margin_top' => 10,
'orientation' => 'L',
'title' => $title,
'author' => \config('app.name', 'Startcoq'),
'watermark' => '',
'show_watermark' => false,
'watermark_font' => 'sans-serif',
'display_mode' => 'fullpage',
'watermark_text_alpha' => 0.1,
'custom_font_dir' => '',
'custom_font_data' => [],
'auto_language_detection' => false,
'temp_dir' => rtrim(
ini_get('upload_tmp_dir') ? ini_get('upload_tmp_dir') : sys_get_temp_dir(),
DIRECTORY_SEPARATOR
),
'pdfa' => false,
'pdfaauto' => false,
]
);
return $pdf;
}
/**
* Generate a pdf with multiple products.
*
* @param array<integer> $printMultiProduct
* @param string $title
* @return \Mccarlosen\LaravelMpdf\LaravelMpdf
*/
public static function generateMultipleProductPdf(array $printMultiProduct, string $title)
{
/** @var \Mccarlosen\LaravelMpdf\LaravelMpdf|null */
$pdf = null;
// * Loop all Model ids.
foreach ($printMultiProduct as $productId) {
/** @var \App\Models\Product */
$product = Product::query()->where('id', $productId)->firstOrFail();
// * If not yet defined, set the first page.
if (is_null($pdf)) {
$pdf = Product::generateProductPdf($product, $title);
} else {
// * Add another page and add HTML from view to this.
$pdf->getMpdf()->AddPage();
$pdf->getMpdf()->WriteHTML((string)view(
'documents.product-body',
['product' => (object)(new ProductResource($product))->toArray()],
));
}
}
return $pdf;
}
/**
* Fetch and stream export file.
*
* @param string $filepath
* @return \Symfony\Component\HttpFoundation\StreamedResponse
* @throws \Error If file storage config is wrong.
*/
public static function downloadPrivateExportFile(string $filepath): StreamedResponse
{
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$disk = Storage::disk('export');
if (!($disk instanceof \Illuminate\Filesystem\FilesystemAdapter)) {
throw new \Error('Le disque de stockage doit être un système de fichiers local');
}
if (!$disk->exists($filepath)) {
\abort(400, trans('Le fichier demandé est périmé et n\'existe plus'));
}
return \response()->stream(function () use ($disk, $filepath) {
echo $disk->get($filepath);
}, 200, [
'Cache-Control' => 'public, must-revalidate, max-age=0',
'Content-Description' => 'File Transfer',
'Content-Type' => $disk->mimeType($filepath),
'Expires' => 'Sat, 26 Jul 1997 05:00:00 GMT',
'Last-Modified' => gmdate('D, d M Y H:i:s') . ' GMT'
])->send();
}
// * GETTERS
/**
* Get origins_enum attribute
*
* @return \Illuminate\Support\Collection<\App\Enums\Ingredients\OriginEnum>
*/
public function getOriginsEnumAttribute(): Collection
{
return \collect($this->getAttribute('origins'))->filter()
->map(fn (int $originEnumValue) => OriginEnum::make($originEnumValue));
}
/**
* Set origins_enum attribute
*
* @param mixed $value
* @return void
*/
public function setOriginsEnumAttribute(mixed $value): void
{
$this->setAttribute('origins', \collect($value)->filter()
->map(fn ($originEnumValue) => OriginEnum::make($originEnumValue)->value())
->all());
}
/**
* Get claims_enum attribute
*
* @return \Illuminate\Support\Collection<\App\Enums\Ingredients\ClaimEnum>
*/
public function getClaimsEnumAttribute(): Collection
{
return \collect($this->getAttribute('claims'))->filter()
->map(fn (int $originEnumValue) => ClaimEnum::make($originEnumValue));
}
/**
* Set claims_enum attribute
*
* @param mixed $value
* @return void
*/
public function setClaimsEnumAttribute(mixed $value): void
{
$this->setAttribute('claims', \collect($value)->filter()
->map(fn ($claimEnumValue) => OriginEnum::make($claimEnumValue)->value())
->all());
}
// * RELATIONSHIPS
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function productComments()
{
return $this->hasMany(ProductComment::class);
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function brand()
{
return $this->belongsTo(Brand::class);
}
/**
* MorphToMany keywords relation.
*
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
*/
public function productKeywords()
{
return $this->morphToMany(Keyword::class, 'keywordable');
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property integer $id
* @property integer $product_id
* @property string $evaluation
* @property string|null $aspect
* @property string|null $taste
* @property string|null $odor
* @property string|null $texture
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*
* * RELATIONSHIPS
* @property-read \App\Models\Product $product
*/
class ProductComment extends Model
{
use HasFactory;
/**
* @var array
*/
protected $fillable = [
'product_id',
'evaluation',
'aspect',
'taste',
'odor',
'texture',
'created_at',
'updated_at'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
// * RELATIONSHIPS
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function product()
{
return $this->belongsTo(Product::class);
}
}

123
app/Models/Promo.php Normal file
View file

@ -0,0 +1,123 @@
<?php
namespace App\Models;
use App\Enums\Promotions\SourceEnum;
use App\Enums\Promotions\TargetEnum;
use App\Enums\Promotions\AngleEnum;
use App\Enums\Promotions\EventEnum;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property integer $id
* @property integer $user_id
* @property integer $promo_type_id
* @property integer $promo_distribution_network_id
* @property integer $brand_id
* @property string $date
* @property boolean $source
* @property string $shop_name
* @property string $shop_city
* @property string $sector_chief
* @property string $name
* @property string $target
* @property string $angle
* @property string $event
* @property string $url
* @property string $press
* @property string $fair
* @property string $created_at
* @property string $updated_at
* @property string $deleted_at
* @property Brand[] $brands
* @property PromoType $promoType
* @property PromoDistributionNetwork $promoDistributionNetwork
* @property User $user
* @property Brand $brand
*/
class Promo extends Model
{
use HasFactory;
/**
* @var array
*/
protected $fillable = [
'user_id',
'promo_type_id',
'promo_distribution_network_id',
'brand_id',
'date',
'source',
'shop_name',
'shop_city',
'sector_chief',
'name',
'target',
'angle',
'event',
'url',
'press',
'fair',
'created_at',
'updated_at',
'deleted_at'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'source' => SourceEnum::class,
'target' => TargetEnum::class,
'angle' => AngleEnum::class,
'event' => EventEnum::class,
'date' => 'datetime:Y-m-d',
'created_at' => 'datetime:Y-m-d',
'updated_at' => 'datetime:Y-m-d',
'deleted_at' => 'datetime:Y-m-d',
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function brands()
{
return $this->belongsToMany('App\Models\Brand', 'promo_brands');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function brand()
{
return $this->belongsTo('App\Models\Brand');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function promoType()
{
return $this->belongsTo('App\Models\PromoType');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function promoDistributionNetwork()
{
return $this->belongsTo('App\Models\PromoDistributionNetwork');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('App\Models\User');
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace App\Models;
use App\Enums\Promotions\DistributionNetworkEnum;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property integer $id
* @property boolean $name
* @property string $label
* @property string $created_at
* @property string $updated_at
* @property Promo[] $promos
*/
class PromoDistributionNetwork extends Model
{
use HasFactory;
/**
* @var array
*/
protected $fillable = [
'name',
'label',
'created_at',
'updated_at'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'name' => DistributionNetworkEnum::class
];
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function promos()
{
return $this->hasMany('App\Models\Promo');
}
}

54
app/Models/PromoType.php Normal file
View file

@ -0,0 +1,54 @@
<?php
namespace App\Models;
use App\Enums\Promotions\TypeEnum;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property integer $id
* @property boolean $name
* @property string $label
* @property string $created_at
* @property string $updated_at
* @property Promo[] $promos
*/
class PromoType extends Model
{
use HasFactory;
/**
* Indicates if the IDs are auto-incrementing.
*
* @var boolean
*/
public $incrementing = false;
/**
* @var array
*/
protected $fillable = [
'value',
'label',
'created_at',
'updated_at'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'value' => TypeEnum::class
];
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function promos()
{
return $this->hasMany('App\Models\Promo');
}
}

70
app/Models/Survey.php Normal file
View file

@ -0,0 +1,70 @@
<?php
namespace App\Models;
use App\Enums\Surveys\CustomerEnum;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property integer $id
* @property integer $user_id
* @property string $name
* @property string $author
* @property string $comment
* @property string $date
* @property string $documents
* @property boolean $is_distributor
* @property boolean $is_customer
* @property boolean $type
* @property string $created_at
* @property string $updated_at
* @property string $deleted_at
* @property User $user
*/
class Survey extends Model
{
use HasFactory;
/**
* @var array
*/
protected $fillable = [
'user_id',
'name',
'author',
'comment',
'date',
'documents',
'is_distributor',
'is_customer',
'type',
'created_at',
'updated_at',
'deleted_at'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'date' => 'datetime:Y-m-d',
'documents' => 'json',
'is_distributor' => 'boolean',
'is_customer' => 'boolean',
'type' => CustomerEnum::class,
'created_at' => 'datetime:Y-m-d',
'updated_at' => 'datetime:Y-m-d',
'deleted_at' => 'datetime:Y-m-d',
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('App\Models\User');
}
}

43
app/Models/User.php Normal file
View file

@ -0,0 +1,43 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use HasFactory;
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}

View file

@ -0,0 +1,42 @@
<?php
namespace App\Providers;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot(): void
{
// * LOCAL HTTPS FIX
URL::forceScheme('https');
// * HA PROXY FIX
App::singleton('clientIp', function () {
if (
config('app.env') === 'local'
) {
return request()->getClientIp();
} else {
return request()->server('HTTP_X_FORWARDED_FOR') ??
request()->getClientIp();
}
});
// * LOCAL FORCE eg: for Carbon
setlocale(LC_ALL, \sprintf(
'%s_%s.UTF-8',
config('app.locale'),
\strtoupper(config('app.locale'))
));
Carbon::setLocale(config('app.locale'));
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The model to policy mappings for the application.
*
* @var array<class-string, class-string>
*/
protected $policies = [];
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot(): void
{
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Providers;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event to listener mappings for the application.
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
];
/**
* Register any events for your application.
* @return void
*/
public function boot(): void
{
}
/**
* Determine if events and listeners should be automatically discovered.
* @return boolean
*/
public function shouldDiscoverEvents(): bool
{
return false;
}
}

View file

@ -0,0 +1,93 @@
<?php
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
/**
* The path to the "home" route for your application.
*
* This is used by Laravel authentication to redirect users after login.
*
* @var string
*/
public const HOME = '/';
/**
* Define your route model bindings, pattern filters, etc.
*
* @return void
*/
public function boot()
{
$this->configureRateLimiting();
$this->cspReportRoute();
$this->routes(function () {
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
});
}
/**
* Configure the rate limiters for the application.
*
* @return void
*/
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(150)->by(optional($request->user())->id ?: app('clientIp'));
});
RateLimiter::for('web', function (Request $request) {
return Limit::perMinute(150)->by(optional($request->user())->id ?: app('clientIp'));
});
RateLimiter::for('credentials', function (Request $request) {
return Limit::perMinute(30)->by(optional($request->user())->id ?: $request->ip());
});
}
/**
* * Register CSP report URI
*
* Put this function inside RouteServiceProvider,
* and call it in RouteServiceProvider::boot
*
* @return void
*/
private function cspReportRoute()
{
// * CSP REPORT URI, Please add /csp-report to VerifyCsrfToken $except middleware
\Illuminate\Support\Facades\Route::post('/csp-report', function () {
$isCspRequest = request()->header('Content-Type') === 'application/csp-report';
if (!$isCspRequest or !request()->json('csp-report') or !is_array(request()->json('csp-report'))) {
abort(401);
}
// * Flatten if needed
$cspReport = collect(request()->json('csp-report'))
->flatMap(function ($v, $k) {
return [$k => is_array($v) ? collect($v)->flatten()->implode(', ') : $v];
})->all();
// * Filter array keys
$cspReport = collect(array_intersect_key($cspReport, [
'blocked-uri' => '', 'document-uri' => '', 'original-policy' => '',
'referrer' => '', 'violated-directive' => ''
]));
\Illuminate\Support\Facades\Log::warning(sprintf(
"CSP REPORT for %s : \n %s",
$cspReport->get('document-uri'),
$cspReport->implode("\n")
));
})->middleware('throttle:5,2')
->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class)
->name('cspReportUri');
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace App\Providers;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\Finder\SplFileInfo;
class TranslationServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
foreach ($this->collectLocalesStrings() as $locale) {
// * Suported locales
Cache::rememberForever(sprintf('translations.%s', $locale), function () use ($locale) {
$translations = [
'php' => $this->phpTranslations($locale),
'json' => $this->jsonTranslations($locale),
];
return $translations;
});
}
}
/**
* Gather provided langs checking from files
*
* @return Collection
*/
private function collectLocalesStrings(): Collection
{
return collect(File::allFiles(resource_path('lang/')))->flatMap(function (SplFileInfo $file) {
if ($file->getRelativePath() and \strpos($file->getRelativePath(), 'vendor') !== 0) {
return [$file->getRelativePath() => ''];
}
})->keys();
}
/**
* Gather all php translations
*
* @param string $locale
* @return array
*/
private function phpTranslations(string $locale): array
{
$path = resource_path("lang/$locale");
return collect(File::allFiles($path))->flatMap(function ($file) use ($locale) {
$translation = $file->getBasename('.php');
return [$translation => trans($translation, [], $locale)];
})->all();
}
/**
* Gather json translations
*
* @param string $locale
* @return array
*/
private function jsonTranslations(string $locale): array
{
$path = resource_path("lang/$locale.json");
if (is_string($path) && is_readable($path)) {
return json_decode(file_get_contents($path), true) ?? [];
}
return [];
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Providers;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\ServiceProvider;
class ViewServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Paginator::defaultview('modules.pagination');
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace App\Traits\Controllers;
use App\Lib\Helpers\FileStorageHelper;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
trait HasDocuments
{
/**
* Save Model Images
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
private function storeDocuments(Request $request, Model $model): void
{
$model->docs = [];
$model->docs = collect($request->docs)
->map(function ($document) use ($model) {
return FileStorageHelper::storeFile($model, $document);
})->all();
}
}

View file

@ -0,0 +1,229 @@
<?php
namespace App\Traits\Enums;
use Illuminate\Support\Str;
/**
* @author Jérémy Munsch <jeremy.munsch@ldc.fr>
*/
trait BaseEnum
{
// * GENERIC METHODS
/**
* Get enum value
*
* @return string|integer
*/
public function value(): string|int
{
return self::toValues()[$this->name];
}
/**
* Get enum label
*
* @return string
*/
public function label(): string
{
return self::toLabels()[$this->name];
}
// * STATIC FETCH METHODS
/**
* Get enum labels
*
* @return array<string>
*/
public static function toLabels(): array
{
if (empty(self::getCache('labels'))) {
$labels = self::getLabelsConstant();
self::setCache('labels', \collect(self::cases())
->mapWithKeys(function ($enum) use ($labels) {
if (!\is_null($labels) and !isset($labels[$enum->name])) {
throw new \Error('Missing definition label ' . self::class . " for {$enum->name}");
}
return [$enum->name => \trans(
!\is_null($labels) ? $labels[$enum->name] : Str::ucfirst($enum->name)
)];
})->all());
}
return self::getCache('labels');
}
/**
* Get enum values
*
* @return array<integer|string>
*/
public static function toValues(): array
{
if (empty(self::getCache('values'))) {
self::setCache('values', \collect(self::cases())
->mapWithKeys(fn ($enum) => [$enum->name => $enum->value])->all());
}
return self::getCache('values');
}
/**
* Get enums
*
* @return array
*/
public static function toArray(): array
{
if (empty(self::getCache('enums'))) {
$definitions = self::getCustomDefinitions();
self::setCache('enums', \collect(self::cases())
->mapWithKeys(function ($enum) use ($definitions) {
$baseArray = [
'name' => $enum->name,
'label' => $enum->label(),
'value' => $enum->value,
];
foreach ($definitions as $index => $elements) {
$index = Str::lower(Str::singular($index));
if (!isset($elements[$enum->name])) {
throw new \Error("Missing definition {$index} " . self::class . " for {$enum->name}");
}
$baseArray[$index] = $elements[$enum->name];
}
return [$enum->name => (object)$baseArray];
})->all());
}
return self::getCache('enums');
}
/**
* Fetch enum using value
*
* @param \App\Traits\Enums\BaseEnum|integer|string $value
* @return \App\Traits\Enums\BaseEnum|null
*/
public static function make(self|int|string $value): self|null
{
return !\is_object($value) ?
\collect(self::cases())->keyBy('value')->get($value) : $value;
}
/**
* Test if 2 enum are equals
*
* @param object ...$others
* @return boolean
* @throws \Error If provided element are not enums taht extends BaseEnum.
*/
public function equals(object ...$others): bool
{
foreach ($others as $other) {
if (!\in_array(BaseEnum::class, \class_uses_recursive($other))) {
throw new \Error(__METHOD__ .
' requires $enum to use ' . BaseEnum::class);
}
if (
get_class($this) === get_class($other)
&& $this->value === $other->value
) {
return true;
}
}
return false;
}
// * CACHE
/**
* Get cache array
*
* @param string $index
* @return array
*/
private static function getCache(string $index): array
{
self::initCache();
if (!isset($GLOBALS[self::class]['props'][$index])) {
$GLOBALS[self::class]['props'][$index] = [];
}
return $GLOBALS[self::class]['props'][$index];
}
/**
* Set cache array
*
* @param string $index
* @param array $items
* @return void
*/
private static function setCache(string $index, array $items): void
{
self::initCache();
if (!isset($GLOBALS[self::class]['props'][$index])) {
$GLOBALS[self::class]['props'][$index] = [];
}
$GLOBALS[self::class]['props'][$index] = $items;
}
/**
* Init cache if needed
*
* @return void
*/
private static function initCache(): void
{
if (!isset($GLOBALS[self::class])) {
$GLOBALS[self::class] = [
'props' => [
'enums' => [],
'values' => [],
'labels' => []
],
'customProps' => null
];
}
}
// * PRIVATE
/**
* Fetch the LABELS constant if exists
*
* @return array|null
*/
private static function getLabelsConstant(): array|null
{
$class_name = self::class;
try {
$constant_reflex = new \ReflectionClassConstant($class_name, 'LABELS');
return $constant_reflex->getValue();
} catch (\ReflectionException $e) {
return null;
}
}
/**
* Get custom constants
*
* @return array
*/
private static function getCustomDefinitions(): array
{
// ? Has custom props in cache ?
if (\is_null($GLOBALS[self::class]['customProps'])) {
$names = \collect(self::cases())->keyBy('name');
$GLOBALS[self::class]['customProps'] = \collect((new \ReflectionClass(self::class))->getConstants())
->filter(function (mixed $constant, string $key) use ($names) {
if ($key === 'LABELS' or !\is_array($constant)) {
return false;
}
return \collect($constant)->keys()
->filter(fn (string $name) => $names->has($name))->count();
})->all();
}
return $GLOBALS[self::class]['customProps'];
}
}

View file

@ -0,0 +1,112 @@
<?php
namespace App\Traits\Requests;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
trait HasDocumentAndPictureList
{
/**
* Autorised mimtypes
*
* @var string[]
*/
private $autorisedMimTypes = [
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'image/jpeg',
'image/png',
'image/gif',
'application/pdf',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
'text/plain',
];
/**
* Images rules
*
* @param string $name
* @return array
*/
public function documentsRules(
string $name = 'docs',
): array {
return [
"{$name}" => 'present|array',
"{$name}.*" => [
'required',
function (string $attribute, $value, callable $fail) {
return $this->validateDocument($attribute, $value, $fail);
}
]
];
}
/**
* Validate picture.
*
* @param string $attribute
* @param mixed $value
* @param callback $fail
* @return void
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
*/
private function validateDocument(string $attribute, mixed $value, callable $fail)
{
// phpcs:enable
if (!($useStorage = Storage::exists($value)) and !File::exists($value)) {
$fail(trans(":attribute n'existe pas dans le système de fichier"));
return;
}
if (!\in_array($this->getUploadedFileMimeType($value, $useStorage), $this->autorisedMimTypes)) {
$fail(trans(':attribute doit être un document de type :mimetypes', [
'mimetypes' => Str::of($this->autorisedMimTypes)->implode(' ou ')
]));
}
$maxSize = 6 * 1024 * 1024;
if (File::size($value) > $maxSize) {
$fail(trans(':attribute doit être inférieur à :size Mo', ['size' => $maxSize / (1024 * 1024)]));
}
}
/**
* Merges docs fields with request data.
*
* @param string $name
* @return void
*/
protected function mergeDocuments(string $name = 'docs'): void
{
$documents = $this->{$name};
$this->merge([$name => $this->{$name} ?? []]);
$this->merge(["{$name}Validate" => []]);
if (is_array($documents)) {
foreach ($documents as $key => $documentPath) {
// ! Empty document.
if ($documentPath === 'false') {
// * Empty document and exit
$this->merge([$name => []]);
return;
}
$decodedPath = \urldecode($documentPath);
$this->merge([
"{$name}Validate" => array_merge(
$this->{"{$name}Validate"},
[\public_path(ltrim($decodedPath, '/'))]
)
]);
$changedDocument = $this->{$name};
$changedDocument[$key] = ltrim($decodedPath, '/');
$this->merge([
$name => $changedDocument
]);
}
} //end if
}
}

View file

@ -0,0 +1,261 @@
<?php
namespace App\Traits\Requests;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
trait HasPicture
{
/**
* Minimum width
*
* @var integer
*/
private $minWith = 100;
/**
* Maximum width
*
* @var integer
*/
private $maxWith = 3840;
/**
* Minimum height
*
* @var integer
*/
private $minHeight = 100;
/**
* Maximum height
*
* @var integer
*/
private $maxHeight = 2160;
/**
* Image rules
*
* @param string $name
* @param boolean $required
* @param integer|null $minWith
* @param integer|null $minHeight
* @param integer|null $maxWith
* @param integer|null $maxHeight
* @return array
*/
public function pictureRules(
string $name = 'picture',
bool $required = true,
?int $minWith = null,
?int $minHeight = null,
?int $maxWith = null,
?int $maxHeight = null
): array {
$sizes = (object)[
'minWith' => $minWith ?? $this->minWith,
'minHeight' => $minHeight ?? $this->minHeight,
'maxWith' => $maxWith ?? $this->maxWith,
'maxHeight' => $maxHeight ?? $this->maxHeight
];
return [
$name => [
$required ? 'required' : 'nullable',
function (string $attribute, $value, callable $fail) use ($sizes) {
return $this->validatePicture($attribute, $value, $fail, $sizes);
}
]
];
}
/**
* Images rules
*
* @param string $name
* @param boolean $required
* @param integer|null $limitMin
* @param integer|null $limitMax
* @param integer|null $minWith
* @param integer|null $minHeight
* @param integer|null $maxWith
* @param integer|null $maxHeight
* @return array
*/
public function picturesRules(
string $name = 'pictures',
?bool $required = null,
?int $limitMin = null,
?int $limitMax = null,
?int $minWith = null,
?int $minHeight = null,
?int $maxWith = null,
?int $maxHeight = null
): array {
$sizes = (object)[
'required' => $required ?? $this->required,
'limitMin' => $limitMin ?? $this->limitMin,
'limitMax' => $limitMax ?? $this->limitMax,
'minWith' => $minWith ?? $this->minWith,
'minHeight' => $minHeight ?? $this->minHeight,
'maxWith' => $maxWith ?? $this->maxWith,
'maxHeight' => $maxHeight ?? $this->maxHeight
];
return [
"{$name}" => ($required ? 'required' : 'nullable') . "|array|between:$limitMin,$limitMax",
"{$name}.*" => [
($required ? 'required' : 'nullable'),
function (string $attribute, $value, callable $fail) use ($sizes) {
return $this->validatePicture($attribute, $value, $fail, $sizes);
}
]
];
}
/**
* Validate picture.
*
* @param string $attribute
* @param mixed $value
* @param callback $fail
* @param object $sizes
* @return void
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
*/
private function validatePicture(string $attribute, mixed $value, callable $fail, object $sizes)
{
if (!($useStorage = Storage::exists($value)) and !File::exists($value)) {
$fail(trans(":attribute n'existe pas dans le système de fichier"));
return;
}
/** @var string */
$fileFullPath = $this->getUploadedFileFullPath($value, $useStorage);
if (
!\in_array($this->getUploadedFileMimeType($value, $useStorage), [
'image/jpeg', 'image/jpg', 'image/png', 'image/gif'
])
) {
$fail(trans(":attribute doit être une image de type 'image/jpeg,image/jpg,image/png,image/gif'"));
}
$filesize = \filesize($fileFullPath);
$maxSize = 6 * 1024 * 1024;
if ($filesize === false || $filesize > $maxSize) {
$fail(trans(':attribute doit être inférieur à :size Mo', ['size' => $maxSize / (1024 * 1024)]));
}
if (!($dimensions = \getimagesize($fileFullPath)) or !isset($dimensions[0]) or !isset($dimensions[1])) {
$fail(trans(":attribute impossible de valider les dimensions"));
} else {
$width = intval($dimensions[0]);
$height = intval($dimensions[1]);
$xMessage = trans(
":attribute la largeur doit être comprise entre {$sizes->minWith}px et {$sizes->maxWith}px"
);
if ($sizes->minWith === $sizes->maxWith) {
$xMessage = trans(":attribute la largeur doit être de {$sizes->minWith}px");
}
$yMessage = trans(
":attribute la hauteur doit être comprise entre {$sizes->minWith}px et {$sizes->maxWith}px"
);
if ($sizes->minHeight === $sizes->maxHeight) {
$yMessage = trans(":attribute la hauteur doit être de {$sizes->minWith}px");
}
if (($width < $sizes->minWith) or ($width > $sizes->maxWith)) {
$fail($xMessage);
}
if (($height < $sizes->minHeight) or ($height > $sizes->maxHeight)) {
$fail($yMessage);
}
} //end if
}
/**
* Merges picture field with request data.
*
* @param string $name
* @return void
*/
protected function mergePicture(string $name = 'picture'): void
{
$picturePath = $this->{$name};
if (is_string($picturePath)) {
$this->merge(["{$name}Validate" => []]);
// ! Empty picture.
if ($picturePath === 'false') {
// * Empty picture and exit
$this->merge([$name => []]);
return;
}
$decodedPath = \urldecode($picturePath);
$this->merge([
"{$name}Validate" => array_merge(
$this->{"{$name}Validate"},
[\public_path(ltrim($decodedPath, '/'))]
)
]);
$changedPicture = ltrim($decodedPath, '/');
$this->merge([
$name => $changedPicture
]);
} //end if
}
/**
* Merges pictures fields with request data.
*
* @param string $name
* @return void
*/
protected function mergePictures(string $name = 'pictures'): void
{
$pictures = $this->{$name};
if (is_array($pictures)) {
$this->merge(["{$name}Validate" => []]);
foreach ($pictures as $key => $picturePath) {
// ! Empty picture.
if ($picturePath === 'false') {
// * Empty picture and exit
$this->merge([$name => []]);
return;
}
$decodedPath = \urldecode($picturePath);
$this->merge([
"{$name}Validate" => array_merge(
$this->{"{$name}Validate"},
[\public_path(ltrim($decodedPath, '/'))]
)
]);
$changedPicture = $this->{$name};
$changedPicture[$key] = ltrim($decodedPath, '/');
$this->merge([
$name => $changedPicture
]);
}
} //end if
}
/**
* Get the file full path.
*
* @param string $value
* @param boolean $useStorage
* @return string
*/
private function getUploadedFileFullPath(string $value, bool $useStorage): string
{
return !$useStorage ? (realpath($value) ?: $value) : Storage::path($value);
}
/**
* Get the file mimetype.
*
* @param string $value
* @param boolean $useStorage
* @return string|false
*/
private function getUploadedFileMimeType(string $value, bool $useStorage)
{
return !$useStorage ? File::mimeType($value) : Storage::mimeType($value);
}
}

53
artisan Executable file
View file

@ -0,0 +1,53 @@
#!/usr/bin/env php
<?php
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any of our classes manually. It's great to relax.
|
*/
require __DIR__.'/vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Artisan Application
|--------------------------------------------------------------------------
|
| When we run the console application, the current CLI command will be
| executed in this console and the response sent back to a terminal
| or another output device for the developers. Here goes nothing!
|
*/
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArgvInput,
new Symfony\Component\Console\Output\ConsoleOutput
);
/*
|--------------------------------------------------------------------------
| Shutdown The Application
|--------------------------------------------------------------------------
|
| Once Artisan has finished running, we will fire off the shutdown events
| so that any final work may be done by the application before we shut
| down the process. This is the last thing to happen to the request.
|
*/
$kernel->terminate($input, $status);
exit($status);

55
bootstrap/app.php Normal file
View file

@ -0,0 +1,55 @@
<?php
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| The first thing we will do is create a new Laravel application instance
| which serves as the "glue" for all the components of Laravel, and is
| the IoC container for the system binding all of the various parts.
|
*/
$app = new Illuminate\Foundation\Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
/*
|--------------------------------------------------------------------------
| Bind Important Interfaces
|--------------------------------------------------------------------------
|
| Next, we need to bind some important interfaces into the container so
| we will be able to resolve them when needed. The kernels serve the
| incoming requests to this application from both the web and CLI.
|
*/
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
/*
|--------------------------------------------------------------------------
| Return The Application
|--------------------------------------------------------------------------
|
| This script returns the application instance. The instance is given to
| the calling script so we can separate the building of the instances
| from the actual running of the application and sending responses.
|
*/
return $app;

2
bootstrap/cache/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

91
composer.json Normal file
View file

@ -0,0 +1,91 @@
{
"name": "laravel/laravel",
"type": "project",
"description": "The Laravel Framework.",
"keywords": [
"framework",
"laravel"
],
"license": "MIT",
"require": {
"php": "^8.1",
"carlos-meneses/laravel-mpdf": "^2.1",
"diglactic/laravel-breadcrumbs": "^8.0",
"guzzlehttp/guzzle": "^7.2",
"kwaadpepper/image-resizer": "^2.1",
"laravel/framework": "^10.0",
"laravel/tinker": "^2.8"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",
"kitloong/laravel-migrations-generator": "^6.8",
"krlove/eloquent-model-generator": "dev-l10-compatibility",
"laravel-lang/common": "^4.0",
"laravel/pint": "^1.0",
"laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0",
"phpunit/phpunit": "^10.0",
"slevomat/coding-standard": "^8.12",
"spatie/laravel-ignition": "^2.0",
"thedoctor0/laravel-factory-generator": "^1.2"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/laravel-shift/eloquent-model-generator.git"
}
],
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"refresh": [
"@php artisan optimize:clear",
"@php artisan clear-compiled",
"@php artisan view:clear",
"@php artisan route:clear",
"@php artisan config:clear",
"@php artisan cache:clear"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
]
},
"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
}
},
"minimum-stability": "stable",
"prefer-stable": true
}

10313
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

216
config/app.php Normal file
View file

@ -0,0 +1,216 @@
<?php
use Illuminate\Support\Facades\Facade;
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application. This value is used when the
| framework needs to place the application's name in a notification or
| any other location as required by the application or its packages.
|
*/
'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
| your application so that it is used when running Artisan tasks.
|
*/
'url' => env('APP_URL', 'http://localhost'),
'asset_url' => env('ASSET_URL', '/'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. We have gone
| ahead and set this to a sensible default for you out of the box.
|
*/
'timezone' => 'Europe/Paris',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by the translation service provider. You are free to set this value
| to any of the locales which will be supported by the application.
|
*/
'locale' => 'fr',
/*
|--------------------------------------------------------------------------
| Application Fallback Locale
|--------------------------------------------------------------------------
|
| The fallback locale determines the locale to use when the current one
| is not available. You may change the value to correspond to any of
| the language folders that are provided through your application.
|
*/
'fallback_locale' => 'fr',
/*
|--------------------------------------------------------------------------
| Faker Locale
|--------------------------------------------------------------------------
|
| This locale will be used by the Faker PHP library when generating fake
| data for your database seeds. For example, this will be used to get
| localized telephone numbers, street address information and more.
|
*/
'faker_locale' => 'fr_FR',
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is used by the Illuminate encrypter service and should be set
| to a random, 32 character string, otherwise these encrypted strings
| will not be safe. Please do this before deploying an application!
|
*/
'key' => env('APP_KEY'),
'cipher' => 'AES-256-CBC',
/*
|--------------------------------------------------------------------------
| 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' => 'file',
// 'store' 'redis'
],
/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers
|--------------------------------------------------------------------------
|
| The service providers listed here will be automatically loaded on the
| request to your application. Feel free to add your own services to
| this array to grant expanded functionality to your applications.
|
*/
'providers' => [
/*
* Laravel Framework Service Providers...
*/
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
Illuminate\Cookie\CookieServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
Illuminate\Filesystem\FilesystemServiceProvider::class,
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
Illuminate\Hashing\HashServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class,
Illuminate\Pagination\PaginationServiceProvider::class,
Illuminate\Pipeline\PipelineServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
Illuminate\Session\SessionServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
/*
* Package Service Providers...
*/
Krlove\EloquentModelGenerator\Provider\GeneratorServiceProvider::class,
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\TranslationServiceProvider::class,
App\Providers\ViewServiceProvider::class,
],
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
|
| This array of class aliases will be registered when this application
| is started. However, feel free to register as many as you wish as
| the aliases are "lazy" loaded so they don't hinder performance.
|
*/
'aliases' => Facade::defaultAliases()->merge([])->toArray(),
];

Some files were not shown because too many files have changed in this diff Show more