Compare commits

..

269 commits

Author SHA1 Message Date
669ee7d736
feat(deployment): enhance deployment configuration with health checks and resource limits
Added readiness, liveness, and startup probes for improved application health monitoring.
Configured resource requests and limits for better resource management.
2026-11-06 12:23:21 +01:00
01bca3a604
chore(manifest): update configuration for maintenance and session
Added APP_MAINTENANCE_DRIVER and APP_MAINTENANCE_STORE.
Reordered CACHE_STORE and SESSION_DRIVER for clarity.
2026-11-06 11:47:27 +01:00
630646c47f
docs: remove domain md file 2026-11-06 11:26:29 +01:00
dea6dc8a63
feat(test): add unit tests for ImageOptimizerService
- Implemented tests to verify the functionality of the ImageOptimizerService.
- Tests cover optimization of supported image formats, handling of unsupported files, and cleanup of temporary files.
- Added functionality to store optimized images onto disk using a storage service.
2026-11-06 08:51:44 +01:00
3ea72e522f
chore(migration): remove image optimization migration file
This migration file for optimizing product and recipe images has been deleted.
2026-11-06 08:24:30 +01:00
69e8315bd4
feat(migration): add image optimization for products and recipes
This migration optimizes images for products and recipes by resizing and converting them to JPEG format.
2026-11-06 08:12:59 +01:00
3008299cd7
chore(dependencies): add intervention/image package 2026-11-06 08:12:19 +01:00
680f0d648a
style(Recipes): format conditional expression for readability
Improves the readability of the conditional expression in the getColumns function.
2026-11-05 18:20:23 +01:00
64ca29fefc
refactor(PublicS3Disk): change method call to retrieve public URL
Updated the publicUrlOf method to use the correct adapter method for generating the public URL.
2026-11-05 18:15:36 +01:00
779fc59993
refactor(UpdateProductRequest): simplify file handling logic
Refactor the file handling logic in UpdateProductRequest by extracting
picture and PDF resolution into separate methods for better readability
and maintainability.
2026-11-05 18:14:37 +01:00
aff271570e
refactor: published on recipes and products 2026-11-05 18:06:33 +01:00
db5c95f9a1
feat(duplicate): add product and recipe duplication functionality
This commit introduces the ability to duplicate products and recipes,
including handling of associated files and unique naming conventions.
2026-11-05 17:46:14 +01:00
531a5c626a
style(Show): format SafeHtml component for better readability 2026-11-05 17:19:34 +01:00
9bb3d1a103
refactor(breadcrumbs): improve breadcrumb handling for products and recipes
- Adjusted condition to check for ProductDto instance.
- Added duplicate breadcrumb handling for products and recipes.
2026-11-05 17:17:26 +01:00
ca9c0fbc4e
feat(policy): allow ProductDto and RecipeDto in policies
Updated ProductPolicy and RecipePolicy to accept ProductDto in addition to Product. This change enhances the flexibility of the policies by allowing DTOs to be used for authorization checks.

refactor(provider): simplify service provider bindings

Refactored AppServiceProvider to simplify the binding of ProductDirectory and RecipeDirectory. Removed unnecessary dependencies and improved clarity.

feat(route): update route bindings for front-end

Changed route bindings for products and recipes to use frontProduct and frontRecipe. This improves clarity and consistency in route handling.

fix(breadcrumbs): update breadcrumb checks for DTOs

Updated breadcrumb generation to check for ProductDto and RecipeDto instead of their model counterparts. This ensures breadcrumbs function correctly with the new DTO structure.

refactor(types): remove FilePath type and update schemas

Removed the FilePath type definition and updated related schemas to use string directly. This simplifies type handling for file paths.

style(entities): standardize naming conventions

Updated naming conventions in Product and Recipe entities to use snake_case for consistency across the codebase.

feat(frontend): update product and recipe pages for new DTO structure

Modified front-end components to accommodate changes in the product and recipe data structures, ensuring compatibility with the new DTOs.
2026-11-05 16:49:42 +01:00
481d80c8ea
refactor(requests): simplify nutritional values handling in product requests
Updated the handling of nutritional values in StoreProductRequest and UpdateProductRequest
to improve clarity and maintainability. The logic for processing nutritional values has been
centralized into a dedicated method, enhancing code readability.
2026-11-05 13:25:39 +01:00
8f9bf1e861
refactor(NutritionalValuesCast): enhance validation and error handling
Improved validation logic for nutritional values and added error handling for invalid inputs.
2026-11-05 12:57:17 +01:00
b4b1dd21ac
refactor(NutritionalValue): improve value validation logic
Refactored the constructor to use strict comparison for value validation.
2026-11-05 12:56:14 +01:00
bb454c3684
feat(factory): update Product and Recipe factories for image paths and nutritional values
- Changed image URL generation to use UUIDs for consistency.
- Updated nutritional values to use new NutritionalValue objects.
2026-11-05 12:55:37 +01:00
47671eaa6f
refactor(Product): remove unused StorageService references
Cleaned up the Product model by removing the StorageService dependency and related deletion logic.
2026-11-05 12:34:57 +01:00
19a2af8d5b
refactor(PublicS3Disk): improve publicUrlOf method logic
Refactor the publicUrlOf method to enhance readability and maintainability.
2026-11-05 12:19:07 +01:00
bc69c9f64d
feat(products): implement GetBoPaginatedProductsQuery and input/output classes
feat(recipes): implement GetBoPaginatedRecipesQuery and input/output classes

feat(requests): add table parameter resolution for products and recipes
2026-11-05 12:17:58 +01:00
6c691f77fc
feat(queries): add new product and recipe queries with inputs
- Implement GetCategoriesProductsQuery and GetCategoriesProductsQueryInput for product category retrieval.
- Implement GetRelatedProductsQuery and GetRelatedProductsQueryInput for related product fetching.
- Implement GetFrontRecipesQuery and GetFrontRecipesQueryInput for front-end recipe listing.
- Implement GetRelatedRecipesQuery and GetRelatedRecipesQueryInput for related recipe fetching.
- Update ProductController and RecipeController to utilize new query classes.
- Enhance IndexProductRequest and IndexRecipeRequest to convert requests to query inputs.
2026-11-05 12:02:36 +01:00
04a9f97719
feat(recipes): implement CRUD actions for recipes
- Added actions for creating, updating, deleting, force deleting, and restoring recipes.
- Introduced input classes for each action to handle data validation and transfer.
- Implemented DTOs for recipes to facilitate data transfer between layers.
- Updated RecipeController to utilize new actions and input classes.
- Enhanced recipe model to manage picture URLs and storage.
- Updated frontend components to handle new data structures and display images correctly.
2026-11-05 11:54:19 +01:00
57faeada75
feat(dto): add CategoryProductsDto, ProductDto, and ProductSummaryDto
Add new DTOs for handling product and category data structures.
These DTOs facilitate data transfer and serialization for products and categories.

refactor(controller): update ProductController to use executor pattern

Refactor ProductController to utilize an executor for handling product creation,
updating, and deletion, improving code organization and maintainability.

refactor(controller): update RecipeController to use executor pattern

Refactor RecipeController to utilize an executor for handling recipe creation,
improving code organization and maintainability.

chore: remove MediaController

Remove MediaController as it is no longer needed in the application.

feat(request): enhance StoreProductRequest and UpdateProductRequest

Enhance StoreProductRequest and UpdateProductRequest to include methods for
transforming request data into input objects for actions.

chore: add ActionExecutorService for executing actions and queries

Introduce ActionExecutorService to handle the execution of actions and queries
with logging and transaction management.

chore(deps): update composer dependencies

Update composer dependencies to include webmozart/assert for improved type assertions.
2026-11-05 11:37:20 +01:00
da93f3e59e
feat(PublicS3Disk): initialize with S3 public endpoint
Updated PublicS3Disk to accept S3 public endpoint in constructor.
Enhanced publicUrlOf method to generate public URLs using the adapter.
2026-11-05 09:07:07 +01:00
1e670eb1c2
fix(MediaController): improve S3 URL generation logic
Ensure proper handling of S3 disk type and URL construction.
2026-11-05 07:57:14 +01:00
b67e4cff9f
chore(middleware): fix namespace for AdminSecureMiddleware 2026-11-05 07:40:39 +01:00
75609b7c26
chore(migration): remove obsolete model file migration script
This migration script is no longer needed and has been deleted.
2026-11-04 18:01:14 +01:00
1a09fbf269
chore(manifest): update AWS_URL to use APP_OVH_S3_URL
This change updates the AWS_URL configuration to use the APP_OVH_S3_URL variable for better clarity.
2026-11-04 18:00:57 +01:00
f21563522b
fix(migration): improve S3 file migration handling
Enhance the migration process to check multiple S3 disks for file existence
and handle exceptions more gracefully.
2026-11-04 17:44:29 +01:00
c31a2d5a08
chore(composer): update league/uri and symfony packages to latest versions
Updated dependencies to ensure compatibility and access to new features.
2026-11-04 17:32:26 +01:00
c3cee9910b
chore: integrate PublicS3Disk and PrivateS3Disk classes
- Added PublicS3Disk and PrivateS3Disk classes for S3 disk management.
- Updated ProductController and RecipeController to utilize PublicS3Disk for file uploads.
- Modified MediaController to redirect to S3 URLs.
- Enhanced migration script to support file migration to new S3 structure.
- Updated filesystem and storage service configurations for public/private S3 disks.
2026-11-04 16:42:20 +01:00
39bb68bbc8
chore(manifest): add SESSION_CONNECTION variable to manifest
This change introduces a new environment variable for session connection configuration.
2026-11-04 12:20:17 +01:00
22e97e0bc8
chore(manifest): add CACHE_PREFIX variable to manifest
Updated the manifest to include a CACHE_PREFIX variable for better cache management.
2026-11-04 11:50:51 +01:00
905666e168
chore(manifest): update Redis client from predis to phpredis
This change updates the Redis client configuration to use phpredis for improved performance and compatibility.
2026-11-04 11:23:29 +01:00
66f5d17480
chore(manifest): update environment variables for AWS and Redis
Added AWS configuration and Redis settings to the manifest.
2026-11-04 11:15:55 +01:00
ac51908d5e
chore(docker): update frontend build process and dependencies
Replaced npm with bun for package management and build.
2026-11-04 10:59:16 +01:00
9c5bba55c0
chore(deps): update dependencies and add overrides for Tailwind CSS
- Updated various dependencies to their latest versions.
- Added overrides for @tailwindcss/typography to specify postcss-selector-parser version.
2026-11-04 10:43:48 +01:00
Jérémy
d54b8dda51 Merge branch 'externalize-model-files' into 'develop'
Externalize files using new built in house dep

See merge request ldc-web/projets/ovoteam!50
2026-11-04 10:32:13 +01:00
284907c2a6
feat(migration): add migration for model files to storage service
This migration handles the transfer of model files for products and recipes to the storage service.
2026-11-04 10:30:40 +01:00
be253a969e
chore(composer): update uwil/storage-laravel to version 1.0.2
chore(phpstan): refine PHPDoc error message for clarity
2026-11-04 10:18:52 +01:00
522cf7a87e
style: improve text wrapping and overflow handling in components
- Enhanced text wrapping and overflow handling in BreadcrumbsBase, ItemCard, PageHeader, Show (Products), and Show (Recipes) components for better responsiveness and readability.
2026-11-04 10:18:51 +01:00
e55889369b
refactor(requests): simplify file validation rules
Updated the UpdateProductRequest and UpdateRecipeRequest to use built-in validation rules for file uploads, removing custom validation methods for pictures and PDFs. This change streamlines the validation process and improves code readability.

refactor(helper): remove unused FileStorageHelper

Deleted the FileStorageHelper class as it was no longer needed, simplifying the codebase.

refactor(toolbox): clean up ToolboxHelper methods

Removed unused methods and simplified existing ones in ToolboxHelper to enhance maintainability.

feat(models): add translation search method to Product and Recipe

Added a new method `whereTranslationLike` to both Product and Recipe models to facilitate searching translations.

chore(config): add storage service prefix configuration

Introduced a new configuration option for storage service prefix to allow customization of media paths.

fix(validation): add file existence validation message

Added a French translation for the file existence validation message to improve user feedback.

feat(routes): add media route for file access

Implemented a new route for accessing media files through a MediaController, enhancing file management capabilities.
2026-11-04 10:18:51 +01:00
26723b47f4
chore: update dependencies to latest versions
This update includes various dependency upgrades to improve stability and security.
2026-11-04 10:18:51 +01:00
e8cd693cb6
chore: update dependencies for improved stability
Updated various dependencies to their latest versions to enhance stability and security.
2026-11-04 10:18:50 +01:00
5a65ea9414
fix: restore dynamic pages and configuration 2026-11-04 10:18:50 +01:00
81be02c717
refactor(RecipeController, Recipe): externalize picture storage logic 2026-11-04 10:18:50 +01:00
dee327ce56
refactor(ProductController): externalize file storage logic
Updated product storage logic to utilize StorageService for handling file uploads and removals.
2026-11-04 10:18:49 +01:00
4745eb493f
chore: update AWS configuration and add storage service
- Set AWS access keys and endpoint in .env.example
- Add FilePathCast for handling file paths
- Update composer.json to include new dependencies
- Add storage-service configuration file
- Update French validation messages for nutritional values
2026-11-04 10:18:49 +01:00
Jérémy
efb4584a25 Merge branch '48-update-assets' into 'develop'
fix: 🐛 use object-cover instead of object-contain/object-fit

Closes #48

See merge request ldc-web/projets/ovoteam!49
2026-11-04 10:10:14 +01:00
Corentin Huet
cbc98db487 fix: 🐛 use object-cover instead of object-contain/object-fit 2026-11-04 10:04:01 +01:00
Jérémy
c24a97345d Merge branch '43-update-content' into 'develop'
feat: ✨ update expertise since 1963 page content

Closes #43

See merge request ldc-web/projets/ovoteam!45
2026-11-04 09:58:23 +01:00
Jérémy
4d50aad375 Merge branch '45-seo' into 'develop'
feat: ✨ update page titles and meta descriptions

Closes #45

See merge request ldc-web/projets/ovoteam!46
2026-11-04 09:58:07 +01:00
Jérémy
7bc4f55912 Merge branch '47-range' into 'develop'
feat: ✨ update wide outdoor range page

Closes #47

See merge request ldc-web/projets/ovoteam!48
2026-11-04 09:57:38 +01:00
Corentin Huet
11c07644c0 feat: ✨ update wide outdoor range page 2026-11-02 11:33:39 +01:00
Corentin Huet
1476505853 feat: ✨ update page titles and meta descriptions 2026-10-28 12:32:00 +01:00
Corentin Huet
9f986d9043 feat: ✨ update expertise since 1963 page content 2026-10-28 12:27:19 +01:00
d32fd87d38
fix: made in france banner replacement Closes #36 2026-10-26 15:54:45 +01:00
Vincent BRIDIER
9718b292d9 Merge branch '20-rgpd' into 'develop'
docs: 📝 Added rgpd.md

See merge request ldc-web/projets/ovoteam!21
2026-10-26 13:17:20 +01:00
1fa2019cc4
refactor(requests): update validation rules for details field
Replaced max length validation with MaxStrippedLength for consistency.
2026-10-23 17:38:51 +02:00
f41ffc9257
fix: Fixes #40 resolve WYSIWYG counting 2026-10-23 17:09:57 +02:00
Jérémy
6172f00a38 Merge branch '36-ts-4032-refonte-site-v2-changement-de-visuel-banniere-page-fabrique-en-france' into 'develop'
Resolve "ts-4032 : Refonte site V2 - Changement de visuel bannière page "Fabriqué en France""

Closes #36

See merge request ldc-web/projets/ovoteam!43
2026-10-23 16:41:50 +02:00
ac7d180624
refactor: changement de hero page enagement 2026-10-23 16:39:20 +02:00
Jérémy
4bcb847d9d Merge branch '38-ts-4035-refonte-site-v2-modification-engagements' into 'develop'
Resolve "ts-4035 : Refonte site V2- Modification Engagements"

Closes #38

See merge request ldc-web/projets/ovoteam!42
2026-10-22 14:52:20 +02:00
Jérémy
b7ee6e3ed6 Merge branch '37-ts-4033-refonte-site-v2-modification-contenus-page-fabrique-en-france' into 'develop'
Resolve "ts-4033 : Refonte site V2 - Modification contenus page Fabriqué en France"

Closes #37

See merge request ldc-web/projets/ovoteam!41
2026-10-22 14:50:36 +02:00
3f853b8162
chore(data): remove sample.pdf from seed data
This file is no longer needed in the seed data.
2026-10-22 14:38:36 +02:00
47cd01141d
feat(validation): enhance input validation limits and prepare data for requests
- Added data preparation for validation in StoreProductRequest and StoreRecipeRequest.
- Updated validation rules to include minimum lengths for various fields.
- Integrated limits for word counting in SimpleString, SimpleTextarea, and SimpleWysiwyg components.
2026-10-22 14:35:05 +02:00
85f26b7094
feat(favicon): update web app manifest and add new icons
Updated the site.webmanifest with new app name and icons.
Added new favicon images for better support across devices.
Enhanced favicon handling in the Blade template for back-end.
2026-10-22 12:44:00 +02:00
4cdb52eb10
feat(Commitment): update commitment texts for clarity
Updated the 'gamme' and 'expertise' texts to enhance clarity and consistency with the site's messaging.
2026-10-22 12:30:31 +02:00
84f405d50c
feat(seeder): add dynamic pages and page configs seeders
- Introduced ImportDynamicPagesSeeder for dynamic pages.
- Added JSON files for dynamic pages and page configurations.
2026-10-21 15:33:42 +02:00
f9f2456783
feat(Home): update commitment images and alt texts
Added new images for commitments and updated alt texts for better accessibility.
2026-10-21 15:23:17 +02:00
82ca13647b
feat(Commitment): update commitment images and descriptions
- Replaced outdated images with new ones.
- Improved layout for better responsiveness.
2026-10-21 11:48:50 +02:00
453e262664
feat(TripleCol): add dynamic image size support
Updated the image size property in the TripleCol component to allow for dynamic sizing based on the provided image data.
2026-10-21 11:10:53 +02:00
03bc3a4ce3
feat(manifest): update icons and paths in web app manifest
Added new icon files and updated their paths in the site web manifest for better compatibility.
2026-10-21 10:49:27 +02:00
089304966d
feat(styles): add tertiary color variables and update usage
Added new tertiary color variables to improve styling consistency. Updated text classes in Gamme component to utilize the new colors.
2026-10-21 10:48:45 +02:00
fa48faf634
feat(manifest): update web app manifest and icons
Updated the web app manifest with new name, short name, and icons.
Changed icon paths and added purpose attributes for better compatibility.
2026-10-20 17:28:03 +02:00
c433103588
fix(Commitment): update commitment description for clarity
Refined the text to enhance clarity and emphasize the exclusive sourcing from alternative farming methods.
2026-10-20 17:14:56 +02:00
d6708e0749
feat(MadeInFrance): update description text for clarity
Updated the description to include "Blanc en Neige" for better product representation.
2026-10-20 17:09:28 +02:00
Jérémy
e6084663fc Merge branch 'issue-40' into 'develop'
Corrections produits et recettes

Closes #41 and #42

See merge request ldc-web/projets/ovoteam!40
2026-10-20 15:40:54 +02:00
7c80ac1f11
refactor(seeders): simplify product synchronization logic
Replaced conditional with a more concise when() function for syncing products.
2026-10-20 15:39:00 +02:00
dd6eeb558a
feat(seeders): add recipe synchronization with products
Implement functionality to sync associated recipes for products during CSV import.
2026-10-20 15:34:36 +02:00
b9a0aedceb
refactor(seeders): streamline product and recipe import logic
Refactored the product and recipe import logic to enhance readability and maintainability.
2026-10-20 15:17:26 +02:00
f0ca2622d6
feat(products): add conservation instructions to product details
Added handling for conservation instructions before and after opening, ensuring they are displayed only when available.
2026-10-20 12:43:24 +02:00
a8bf4ea681
style(components): add containerClassName to Section components
Updated Section components to include containerClassName for improved layout consistency.
2026-10-20 12:32:14 +02:00
597e1c720a
refactor: add missing nutritional values on products 2026-10-20 12:23:15 +02:00
325c7a7fd6
style(inputs): update layout alignment in SimpleString component
Adjusted the alignment from justify-between to justify-end for better layout consistency.
2026-10-20 09:18:45 +02:00
764e58b61b
style(inputs): update Label component to use font-bold class
Enhances the visual emphasis of the label in the CompleteInputFile component.
2026-10-20 09:16:47 +02:00
345d0cfbfd
style(inputs): update Label component to use font-bold class
Enhances the visibility of the label in the MultiSelect component.
2026-10-20 09:15:18 +02:00
7482e8cebc
refactor(wysiwyg): improve editor initialization and styling
- Updated editor initialization to use useEditor hook.
- Enhanced styling for better responsiveness and focus handling.
2026-10-20 09:15:04 +02:00
cd2d8ed397
refactor(i18n): replace GetTranslation with trans function
- Updated translation utility to improve consistency and reduce redundancy.
- Removed deprecated useTrans hook and related code.
- Introduced a new Zustand store for managing translations and locale settings.
- Cleaned up unused hooks and files related to localization.
2026-10-19 17:43:33 +02:00
1d628201a6
style(inputs): update Label components to use font-bold class
This change enhances the visual consistency of input labels across various components.
2026-10-19 16:24:15 +02:00
93beae7ade
refactor(inputs): replace Button with span in ModelSelect and MultiSelect
Updated the button elements to span for better semantic usage without altering functionality.
2026-10-19 15:43:36 +02:00
cad48f967c
style(products): adjust button and heading alignment
style(recipes): update button and heading alignment
2026-10-19 15:07:34 +02:00
d4e46b187c
feat: add SEO fields to product and page components
Added seoTitle and seoDescription fields to the Product model and updated relevant components to utilize these fields for improved SEO.
2026-10-19 14:55:17 +02:00
455ec468d8
refactor(seeder): simplify enum value retrieval logic
Updated the methods for retrieving enum values to improve readability and maintainability.
2026-10-19 14:55:03 +02:00
6cfa8d8e27
fix(breadcrumbs): handle missing product and recipe gracefully
Return early instead of throwing exceptions for missing product or recipe
during breadcrumb generation to improve user experience.
2026-10-19 14:39:04 +02:00
b989a45864
refactor: update products csv file and parser 2026-10-19 13:10:17 +02:00
363f0f15e2
feat(seeder): enhance product and recipe import functionality
- Added new columns to the product and recipe import CSVs.
- Implemented robust header validation and mapping for CSV files.
- Refactored data extraction methods for improved readability and maintainability.
- Enhanced error handling during product and recipe creation.
2026-10-19 13:03:27 +02:00
83c5ff3e6f
feat(products): add description field to product model and forms
- Added description field to Product model and database migration.
- Updated StoreProductRequest validation rules for description.
- Modified product factory and seeder to include description.
- Enhanced product forms and show pages to display description.
2026-10-19 11:38:58 +02:00
26c3ff1768
chore: update PHP executable paths and include paths
Removed deprecated PHP executable paths and added new include paths for Faker and Pest.
2026-10-19 11:33:35 +02:00
c35b65d76f
refactor: add "Sans élevage" on Breeding 2026-10-19 10:59:37 +02:00
ad6d2c3413
chore: update dependencies in package-lock.json
- Added @tiptap/extension-text-style version 3.32.0.
- Updated @commitlint packages to version 21.2.4.
- Updated other dependencies to their latest versions.
2026-10-19 10:41:49 +02:00
48cde3db8e
fix: add deleted_at column to users table 2026-10-16 18:04:57 +02:00
ae7e63b810
fix: register Uwil service providers explicitly 2026-10-16 17:43:37 +02:00
d082fd0ca1
Merge branch 'develop' into trunk 2026-10-16 17:26:00 +02:00
16e2040d5c
build: fix 2026-10-16 17:25:55 +02:00
3856151908
Merge branch 'develop' into trunk 2026-10-16 16:59:37 +02:00
4e670b04a4
build: fix 2026-10-16 16:59:28 +02:00
7cdcca687d
fix: add missing middlewares 2026-10-16 16:59:25 +02:00
1cf8f31886
Merge branch 'develop' into trunk 2026-10-16 16:34:25 +02:00
1b4052f088
build: fix 2026-10-16 16:34:16 +02:00
953bf34bb3
Merge branch 'develop' into trunk 2026-10-16 16:24:36 +02:00
f86bf54242
build: fix 2026-10-16 16:24:22 +02:00
7207109d63
build: fix 2026-10-16 15:34:22 +02:00
b2151a6e67
Merge branch 'develop' into trunk 2026-10-16 15:05:12 +02:00
c5e42e0f88
build: fix 2026-10-16 15:05:06 +02:00
a42b822546
Merge branch 'develop' into trunk 2026-10-16 14:46:56 +02:00
449064d5d4
build: fix build image and command 2026-10-16 14:46:27 +02:00
d2c7029779
Merge branch 'develop' into trunk 2026-10-16 14:20:02 +02:00
Jérémy
7a506c7e1a Merge branch 'FFFING_FIXES' into 'develop'
refactor: hudge bump...

See merge request ldc-web/projets/ovoteam!39
2026-10-16 14:19:19 +02:00
3331db4c6c
ci: fix 2026-10-16 14:16:42 +02:00
1316d05b78
ci: updateci file 2026-10-16 14:07:07 +02:00
Jeremy Munsch
9efcb32ee1
refactor: hudge bump !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!111
Fixes #59
2026-10-16 14:00:55 +02:00
Jeremy Munsch
9116a4bf88
fix: products layout 2026-10-16 10:28:11 +02:00
Jeremy Munsch
5117c8e445
refactor: recipe and product requests update 2026-10-16 09:47:39 +02:00
Jeremy Munsch
eb4bf9202b
chore: vscode config update 2026-10-16 09:47:13 +02:00
Jeremy Munsch
d641d61bfd
refactor: prevent crashing on front breadcrumbs 2026-10-16 09:46:45 +02:00
K Muller
2d1ade9d62 Merge branch 'develop' into trunk 2026-10-09 09:56:48 +02:00
Karl M'muller
acdc8fe365 Merge branch '35-update-assets' into 'develop'
feat: ✨ update IAA assets

Closes #35

See merge request ldc-web/projets/ovoteam!38
2026-10-09 07:54:53 +00:00
Corentin Huet
858711fa4f feat: ✨ update IAA assets 2026-10-06 15:26:49 +02:00
Noah Lancien
ee363196dc Merge branch 'develop' into trunk 2026-09-28 15:16:03 +02:00
Noah Lancien
ccaa4de15a fix: 🐛 added url for recipes and products pages 2026-09-28 15:14:52 +02:00
Noah Lancien
4b1bf703c2 Merge branch 'develop' into trunk 2026-09-25 16:13:30 +02:00
Noix
aa40b8a9e4 Merge branch 'issue-29' into 'develop'
style: 💄 wording and picto for commit page

See merge request ldc-web/projets/ovoteam!32
2026-09-25 14:12:36 +00:00
K Muller
3f5537731d style: 💄 wording and picto for commit page 2026-09-25 16:11:20 +02:00
Noah Lancien
6fbd8d82f4 Merge branch 'develop' into trunk 2026-09-25 15:58:52 +02:00
Noah Lancien
27c3d0e87e ci: Use 8.5 + node 24 in Dockerfile 2026-09-25 15:58:31 +02:00
Jeremy Munsch
03fdd79579
Merge branch 'develop' into trunk 2026-09-25 13:25:36 +02:00
Jeremy Munsch
03ffaf5093
refactor: lint fixes 2026-09-25 13:25:27 +02:00
Jeremy Munsch
fce0f69edb
Merge branch 'develop' into trunk 2026-09-25 13:00:21 +02:00
Jeremy Munsch
a9214091c7
chore: npm update 2026-09-25 12:59:50 +02:00
Noah Lancien
42514d9778 Merge branch 'develop' into trunk 2026-09-25 10:53:45 +02:00
Noix
58b8c7d874 Merge branch 'recipes-and-products' into 'develop'
Closes #22, Closes #23

Closes #22 and #23

See merge request ldc-web/projets/ovoteam!25
2026-09-25 08:50:19 +00:00
a57b100e27
chore: composer update hooks 2026-09-21 13:03:16 +02:00
f60ccf9909
chore: vscode settings 2026-09-21 13:03:00 +02:00
1137b191c5
chore: composer update 2026-09-21 13:02:47 +02:00
79cd52bcd6
chore: eslint fixes 2026-09-21 13:00:42 +02:00
c21a9b334b
fix: add missing front breadcrumb route for recipes 2026-09-21 11:52:41 +02:00
fd5992f65f
chore: modification des traductions pour les pages {recipes,products}.index avec les éléments fournis 2026-09-21 11:52:41 +02:00
36a44f0df3
refactor(ProductController, RecipeController): simplify data handling in store and update methods
Improved clarity by introducing a variable for request data before filling the model.
2026-09-21 11:52:40 +02:00
28f447c8ec
refactor: fron and back recipe - products association 2026-09-21 11:52:40 +02:00
b6d08c8bd1
refactor(CarouselContent): reorganize imports and improve pagination logic 2026-09-21 11:51:20 +02:00
d3014b1ac8
style(MobileSlider): adjust dot indicator sizes and spacing
Updated the height and margin of dot indicators for better visibility and alignment.
2026-09-21 11:50:41 +02:00
2610f91bec
style(vscode): update eslint validation for JavaScript files
Enhanced eslint validation to include 'javascriptreact' and 'typescriptreact' for better linting support.
2026-09-21 11:50:41 +02:00
5a7230ca9a
refactor(Recipes): enhance list formatting for ingredients and preparation steps
Improved the formatList function to convert plain text into structured lists.
2026-09-21 11:50:40 +02:00
04d038a61d
refactor(Recipes): replace 'create' and 'update' with 'viewAny' permission check
Updated permission checks in Create and Edit components to use 'viewAny' instead of 'create' and 'update'.
2026-09-21 11:50:40 +02:00
c0f5ae7a77
refactor(Recipes): simplify Form component structure
Removed unnecessary div wrapper around Form component in Create and Edit pages for cleaner code.
2026-09-21 11:50:40 +02:00
3e4201cb98
style(MobileSlider): adjust dot indicator styles and translation 2026-09-21 11:50:40 +02:00
86b0b3cbe3
style(Products): update button label for product view
Removed trailing '>' from button label for clarity.
2026-09-21 11:50:39 +02:00
822d71c640
refactor: remove unused queue methods and update type hints 2026-09-21 11:50:39 +02:00
33494d0360
refactor: phpcs,phpstan fixes with pint pass 2026-09-21 11:50:39 +02:00
5a24c6c8b7
refactor(Create): remove unused ProductModel import
This change cleans up the code by removing an unused import,
improving readability and maintainability.
2026-09-21 11:50:38 +02:00
026b511015
refactor(AppSidebar): remove unused authService import
Cleaned up the AppSidebar component by removing the unused authService import.
2026-09-21 11:50:38 +02:00
8315650407
style(eslint): update no-unused-vars rules for better clarity
Refine ESLint rules for unused variables to improve code quality.
2026-09-21 11:50:38 +02:00
524cba1070
refactor: eslint fixes 2026-09-21 11:50:37 +02:00
7abc85c3e3
feat(wysiwyg): enhance styling and structure for tiptap component
Updated the tiptap class with improved styles for headings, lists, and other elements.
Applied consistent spacing and formatting to enhance readability and presentation.
2026-09-21 11:47:38 +02:00
108e26f9c0
feat(sanitize): add HTML sanitization utility functions
- Introduced DOMPurify for sanitizing HTML inputs.
- Added `sanitizeRichHtml` and `toSanitizedHtml` functions for safe HTML rendering.
- Updated various components to use the new sanitization methods to prevent XSS attacks.
2026-09-21 11:47:38 +02:00
57a9db1fd5
fix(RecipeController): ensure perPage is at least 1 for pagination
This change prevents potential issues with pagination when there are no recipes.
2026-09-21 11:44:53 +02:00
9fc2690e74
feat(products): add mobile filter functionality and UI enhancements
- Implemented mobile filter toggle and drawer for product filters.
- Added MobileFilterGroup component for better filter management.
- Enhanced UI for active filter chips and search bar.
- Improved layout and styling for better responsiveness.

feat(recipes): introduce mobile filter functionality and UI updates

- Added mobile filter toggle and drawer for recipe filters.
- Created MobileFilterGroup component for recipe filtering.
- Updated layout and styling for improved user experience.
- Enhanced responsiveness for recipe display on mobile devices.
2026-09-21 11:44:53 +02:00
b20846f731
feat(ModelActions): add external link handling for front view actions
Enhance GetAction to open specific routes in a new tab with security attributes.
2026-09-21 11:44:52 +02:00
1ad8c00241
fix(link): update rel attribute to 'noopener noreferrer' for security 2026-09-21 11:44:52 +02:00
a1f8e410fe
feat(routes): add duplicate, restore, and force destroy routes for products and recipes
This update introduces new routes for duplicating, restoring, and force deleting products and recipes, enhancing the management capabilities in the application.
2026-09-21 11:43:40 +02:00
444ee46ac0
refactor(breadcrumbs): update product and recipe edit routes
Refactor breadcrumb generation for product and recipe edit routes
to ensure proper instance checks and improve error handling.
2026-09-21 11:43:39 +02:00
1a7f321cff
refactor: initial Recipes and products implementation ok 2026-09-21 11:43:39 +02:00
1a12569395
feat(directory): update ProductDirectory to use ProductId
- Changed the find method to accept ProductId instead of int.
- Added findBySlug method to retrieve products by slug.
- Updated getCategoryProducts method to return CategoryProducts type.

feat(eloquent): implement findBySlug in EloquentProductDirectory

- Implemented findBySlug method to retrieve products by slug.
- Updated find method to handle ProductId.

feat(dto): enhance CategoryProducts to serialize product IDs

- Updated CategoryProducts to serialize product IDs as strings.

feat(controller): modify ProductController to handle search limits

- Set limitPerCategory to -1 for search functionality.

refactor(model): change Product model ID to UUID

- Changed the id field in Product model to UUID.
- Updated related methods to handle UUIDs.

feat(migration): alter products and recipes tables for UUIDs

- Updated migrations to use UUIDs for primary keys in products and recipes tables.
- Adjusted foreign key constraints in product_recipe table.

feat(route): add route model binding for products

- Implemented route model binding for products using ProductId and slug.

chore(seed): remove CSV import calls from MandatorySeeder

- Removed calls to ImportProductsCsvSeeder and ImportRecipesCsvSeeder.

feat(entity): create ProductId and RecipeId value objects

- Added ProductId and RecipeId classes to encapsulate UUIDs.

feat(frontend): update TypeScript interfaces for products

- Updated TypeScript interfaces to reflect changes in product structure.
2026-09-21 11:43:39 +02:00
1200bfea4c
refactor: product.index page with search 2026-09-21 11:43:38 +02:00
e14c43d82f
refactor(routes): correct Inertia render method for demo route
Updated the Inertia render method for the demo route to ensure consistency in code style.
2026-09-21 11:37:14 +02:00
696bd9ed66
refactor(routes): restructure demo route for production check 2026-09-21 11:37:13 +02:00
028f769dbc
feat(products): add create and edit product pages
- Implemented Create and Edit pages for products with forms.
- Added validation and permissions handling for product actions.
- Introduced new product model and related types for better structure.
- Enhanced sidebar registry to remove auth dependency for link resolution.
- Updated routes to handle precognitive requests for product forms.
2026-09-21 11:36:52 +02:00
4139c25e42
feat(middlewares): add HtmlSanitizer and AuthServiceProvider
- Introduced HtmlSanitizer middleware for input sanitization.
- Added AuthServiceProvider for managing authorization policies.
2026-09-21 11:36:52 +02:00
213d0bb568
refactor: restructure application bootstrap files 2026-09-21 11:36:52 +02:00
43c3d55b72
refactor: products.index and recipes.index pages on Bo 2026-09-21 11:36:51 +02:00
a7992f96c8
chore: npm set package name 2026-09-21 11:36:50 +02:00
e39cd1ac61
chore: no emoji on commit message 2026-09-21 11:35:41 +02:00
86fdfa5560
fix: not found page 2026-09-21 11:35:41 +02:00
Jeremy Munsch
d29100bb61
refactor: add recipes and products with data migration from sharepoint doc 2026-09-21 11:35:41 +02:00
527a9f7664
chore: add vscode json elements 2026-09-21 11:35:40 +02:00
Jeremy Munsch
e365d93d1f
chore: pint, phpstanconfig phpcs changes 2026-09-21 11:35:40 +02:00
Jeremy Munsch
5e44470cf5
chore: bump to vite 8 and fix compilation issues 2026-09-21 11:35:40 +02:00
Noah Lancien
5c4670847e Merge branch 'develop' into trunk 2026-09-17 16:33:25 +02:00
Noix
8b85165d42 Merge branch 'fixUrl' into 'develop'
fix: 🐛 url social network

See merge request ldc-web/projets/ovoteam!36
2026-09-17 14:31:33 +00:00
Abdel LAMRANI
377be050da fix: 🐛 url social network 2026-09-15 12:56:19 +02:00
Vincent BRIDIER
f880a72b93 Merge branch 'restrict-demo-route' into 'develop'
fix: 🐛 restrict demo route to local env

See merge request ldc-web/projets/ovoteam!34
2026-09-01 13:05:57 +02:00
Corentin Huet
2ec086c7d7 fix: 🐛 restrict demo route to local env 2026-09-01 12:12:38 +02:00
K Muller
c2eb79882e Merge branch 'develop' into trunk 2026-08-20 10:17:13 +02:00
Karl M'muller
66be9ae6c0 Merge branch 'issue-30' into 'develop'
style: 💄 add picto to commitments

See merge request ldc-web/projets/ovoteam!33
2026-08-20 08:16:42 +00:00
K Muller
f2a59d8356 fix: 🐛 resolve conflicts 2026-08-20 10:14:16 +02:00
Karl M'muller
c3c3af0b03 Merge branch 'issue-31' into 'develop'
style: 💄 add slider product on commitment page

See merge request ldc-web/projets/ovoteam!29
2026-08-20 08:12:13 +00:00
K Muller
76edf3f575 feat: ✨ add labels to range page 2026-08-10 09:40:06 +02:00
K Muller
733a406a30 Merge branch 'develop' into trunk 2026-08-07 11:04:03 +02:00
Karl M'muller
c7ad4cb2ac Merge branch 'issue-27' into 'develop'
fix: 🐛 missing accent on 'a'

See merge request ldc-web/projets/ovoteam!31
2026-08-07 09:03:02 +00:00
K Muller
0319478cf1 fix: 🐛 missing accent on 'a' 2026-08-07 11:02:16 +02:00
K Muller
b995c2b054 Merge branch 'develop' into trunk 2026-08-06 16:22:40 +02:00
Karl M'muller
af232a5fd7 Merge branch 'issue-27' into 'develop'
style: 💄 change commitments section on homepage

See merge request ldc-web/projets/ovoteam!30
2026-08-06 14:21:35 +00:00
K Muller
0d310b0f23 style: 💄 change commitments section on homepage 2026-08-06 15:09:10 +02:00
K Muller
a2da6b0cce style: 💄 add slider product on commitment page 2026-08-06 10:24:34 +02:00
K Muller
fca2500680 Merge branch 'develop' into trunk 2026-08-05 15:42:42 +02:00
Karl M'muller
999995dbad Merge branch 'ts-3788' into 'develop'
style: 💄 commitment page content change

See merge request ldc-web/projets/ovoteam!27
2026-08-05 13:39:49 +00:00
Karl M'muller
7a23a63792 Merge branch 'issue-28' into 'develop'
fix: 💄 change wording + social media order

See merge request ldc-web/projets/ovoteam!28
2026-08-05 13:39:16 +00:00
K Muller
8fd6f539fd fix: 💄 change wording + social media order 2026-08-05 10:59:41 +02:00
K Muller
93dc422e5b style: 💄 commitment page content change 2026-08-04 17:36:12 +02:00
K Muller
e0e5de1145 Merge branch 'develop' into trunk 2026-08-04 17:04:12 +02:00
K Muller
178a8be7ef fix: 🐛 error on package.lock 2026-08-04 17:03:52 +02:00
Karl M'muller
8ab3701670 Merge branch 'issue-32' into 'develop'
feat: ✨ add label slider

See merge request ldc-web/projets/ovoteam!26
2026-08-04 14:52:29 +00:00
K Muller
132b4ba5b4 feat: ✨ add label slider 2026-08-04 15:57:20 +02:00
Jeremy Munsch
64e34ecc4f
fix: eslint 2026-06-26 12:28:24 +02:00
Jeremy Munsch
7f6b1aeb3d
chore: npm update and fix dependencies 2026-06-26 12:12:33 +02:00
Jeremy Munsch
47ce939b9e
fix: tsconfig baseUrl deprecated 2026-06-26 12:11:08 +02:00
Jeremy Munsch
b9c8d9a7f1
fix: incorrectly added Gotham font 2026-06-26 12:10:48 +02:00
Karl M'muller
92f53d2477 Merge branch 'develop' into 'trunk'
Develop

See merge request ldc-web/projets/ovoteam!23
2026-06-25 13:09:11 +00:00
Karl M'muller
f03749e20c Merge branch 'close-24' into 'develop'
Closes #24

See merge request ldc-web/projets/ovoteam!22
2026-06-25 13:07:40 +00:00
Jeremy Munsch
67b8a56126
fix: remove bullet points 2026-06-25 12:39:51 +02:00
Jeremy Munsch
54843370e2
refactor: change hero slide size factor and rendering 2026-06-25 12:28:19 +02:00
Jeremy Munsch
913a49b641
chore: add watch to package.json 2026-06-25 11:32:14 +02:00
Jeremy Munsch
d3fe410952
refactor: change cookinotice disposition in order to be iso as prod 2026-06-25 11:28:39 +02:00
Jeremy Munsch
c2516cc295
chore: update oeuf to Œuf 2026-06-25 11:21:23 +02:00
Jeremy Munsch
686b965f9b
fix: default page using un declared route 2026-06-25 11:15:18 +02:00
Jeremy Munsch
d2a3b61d75
fix: ignore breadcrumb route declarations for dynamic pages as a dirty patch 2026-06-25 10:05:31 +02:00
Jeremy Munsch
893f870735
chore: add public/favicon-back to gitignore 2026-06-25 10:04:22 +02:00
Noah Lancien
eece6bb617 docs: 📝 Added rgpd.md 2026-06-05 16:48:19 +02:00
K Muller
f43cb28a7d Merge branch 'develop' into trunk 2026-06-04 09:05:19 +02:00
karl.muller
5874ea315a Merge branch '25-fix-footer-links' into 'develop'
fix: 🐛 fix wrong URL on footer links

See merge request ldc-web/projets/ovoteam!20
2026-06-04 07:04:47 +00:00
Corentin Huet
dea14e0491 fix: 🐛 fix wrong URL on footer links 2026-06-03 15:02:00 +02:00
K Muller
b2e6a25a92 Merge branch 'develop' into trunk 2026-06-01 10:48:33 +02:00
karl.muller
97e7d7e33a Merge branch 'mailer' into 'develop'
fix: 🐛 change default mailer to smtp

See merge request ldc-web/projets/ovoteam!18
2026-06-01 08:47:47 +00:00
K Muller
5bca572e67 Merge branch 'develop' into trunk 2026-05-28 11:56:09 +02:00
karl.muller
264be08046 Merge branch '24-re7-feedback' into 'develop'
fix: 🐛 resolve various bugs after re7 review

See merge request ldc-web/projets/ovoteam!19
2026-05-28 09:55:37 +00:00
Alexis Gousseau
c4fd2b048e
fix: 🐛 change default mailer to smtp 2026-05-27 11:07:56 +02:00
Corentin Huet
120db17596 fix: 🐛 resolve various bugs after re7 review 2026-05-26 14:52:59 +02:00
Noah Lancien
c747288704 Merge branch 'develop' into trunk 2026-05-04 17:38:56 +02:00
Noix
324e796fc6 Merge branch '24-content' into 'develop'
feat: ✨ update lorem ispum content with real content

See merge request ldc-web/projets/ovoteam!17
2026-05-04 15:37:48 +00:00
Corentin Huet
4512798e44 feat: ✨ update lorem ispum content with real content 2026-04-29 12:55:48 +02:00
karl.muller
379ffe003f Merge branch 'issue-17' into 'develop'
fix: 🐛 fixes after re7

See merge request ldc-web/projets/ovoteam!15
2026-04-21 10:11:52 +00:00
K Muller
fc472db322 fix: 🐛 fixes after re7 2026-04-21 10:54:38 +02:00
karl.muller
672af76d4b Merge branch 'fixes' into 'develop'
style: 💄 after re7 #17

See merge request ldc-web/projets/ovoteam!14
2026-04-16 07:11:58 +00:00
K Muller
ecae3660d0 style: 💄 after re7 #17 2026-04-15 09:31:53 +02:00
karl.muller
4eca9c7d24 Merge branch 'fix-session-driver' into 'develop'
fix: 🐛 add variable to manifest for session driver, default changed into the session.php

See merge request ldc-web/projets/ovoteam!13
2026-04-09 09:17:48 +00:00
K Muller
cdc7e86682 fix: 🐛 add variable to manifest for session driver, default changed into the session.php 2026-04-09 11:16:49 +02:00
karl.muller
cb9d7ac0d5 Merge branch 'addK8s' into 'develop'
feat: ✨ add k8s laravel package

See merge request ldc-web/projets/ovoteam!12
2026-04-09 08:34:39 +00:00
K Muller
4421d8e770 feat: ✨ add k8s laravel package 2026-04-09 10:31:17 +02:00
karl.muller
cc2c137b26 Merge branch '2026-fixes' into 'develop'
fix: 🐛 add seo title translated for each page

See merge request ldc-web/projets/ovoteam!11
2026-04-09 08:13:46 +00:00
K Muller
05af5cf83c fix: 🐛 add seo title translated for each page 2026-04-09 10:12:41 +02:00
karl.muller
083e89364b Merge branch 'fix-manifest' into 'develop'
fix: 🐛 missing middleware on manifest + missing favicon front

See merge request ldc-web/projets/ovoteam!10
2026-04-08 14:55:02 +00:00
K Muller
9aa84edcf3 fix: 🐛 missing middleware on manifest + missing favicon front 2026-04-08 16:51:52 +02:00
K Muller
76a42c17f6 fix: 🐛 error on Dockerfile + wrong favicon 2026-04-08 14:46:14 +02:00
K Muller
af13d298fd fix: 🐛 missing husky registry in Dockerfile 2026-04-08 14:27:09 +02:00
karl.muller
433edf13c8 Merge branch 'fix-linters' into 'ovoteam-2026'
fix: :style: 0 lint error

See merge request ldc-web/projets/ovoteam!9
2026-04-08 12:14:47 +00:00
K Muller
2f90c82d74 fix: :style: 0 lint error 2026-04-08 14:05:46 +02:00
karl.muller
6a82a59bc5 Merge branch 'karl' into 'ovoteam-2026'
feat: ✨ add :

See merge request ldc-web/projets/ovoteam!8
2026-04-07 14:22:07 +00:00
K Muller
db25bec3e5 feat: ✨ add :
- sentry
- variables in manifest
- some dynamic pages
- breadcrumbs
- dynamic page resolver
2026-04-07 16:21:09 +02:00
K Muller
f42dc37674 feat: ✨ add Recaptcha provider / form 2026-04-03 13:56:21 +02:00
K Muller
f2a4a367e0 feat: :construct: front pages + components 2026-04-02 17:29:11 +02:00
K Muller
a7d30e8353 feat: ✨ home page done 2026-03-30 14:29:49 +02:00
K Muller
9a404dd61a wip: on component 2026-03-30 14:29:00 +02:00
K Muller
32c6a9391d chore: 📦 install dynamic pages 2026-03-30 14:20:47 +02:00
K Muller
2dd2c78fa7 chore: 📦 install backend 2026-03-30 14:17:31 +02:00
K Muller
eef93adb2c chore: 📦 update laravel-common > 5.1.1 2026-03-30 14:12:01 +02:00
K Muller
a6370dcb47 first commit 2026-03-16 12:36:44 +01:00
738 changed files with 118157 additions and 2017 deletions

24
.editorconfig Normal file
View file

@ -0,0 +1,24 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4
[*.scss]
indent_size = 2
[*.{tsx,ts,cjs}]
indent_size = 2

79
.env.example Normal file
View file

@ -0,0 +1,79 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
SAML2_SP_CERT_x509=
SAML2_SP_CERT_PRIVATEKEY=
SAML2_CONTACT_TECHNICAL_EMAIL=
SAML2_CONTACT_SUPPORT_EMAIL=
SAML2_DEFAULT_TENANT_UUID=
AWS_ACCESS_KEY_ID=minioadmin
AWS_SECRET_ACCESS_KEY=minioadmin
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=ovoteam
AWS_ENDPOINT=http://127.0.0.1:9000
AWS_URL=http://127.0.0.1:9000/ovoteam
AWS_USE_PATH_STYLE_ENDPOINT=true
VITE_APP_NAME="${APP_NAME}"
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=
MEILISEARCH_KEY=
STORAGE_SERVICE_PREFIX=media

4
.eslintignore Normal file
View file

@ -0,0 +1,4 @@
node_modules
public
vendor
resources/ts/components/ui/

80
.eslintrc.cjs Normal file
View file

@ -0,0 +1,80 @@
module.exports = {
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
project: "./tsconfig.json",
},
settings: {
react: {
version: "detect",
},
"import/resolver": {
typescript: {
project: "./tsconfig.json",
},
alias: {
map: [
["@", "./resources/ts"],
["&", "./resources/assets"],
],
extensions: [".ts", ".tsx", ".js", ".jsx"],
},
},
},
env: {
browser: true,
es2021: true,
node: true,
},
ignorePatterns: ["resources/ts/components/ui/**"],
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
],
plugins: ["react", "react-hooks", "@typescript-eslint", "import", "prettier"],
rules: {
"no-undef": "off",
"react/display-name": "off",
"react/jsx-key": "off",
"react/jsx-filename-extension": ["error", { extensions: [".tsx", ".jsx"] }],
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"react/require-default-props": "off",
"import/extensions": [
"error",
"ignorePackages",
{
ts: "never",
tsx: "never",
js: "never",
jsx: "never",
},
],
"import/order": "off",
"prettier/prettier": "error",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
"no-console": [
"warn",
{
allow: ["warn", "error"],
},
],
},
};

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

40
.gitignore vendored Normal file
View file

@ -0,0 +1,40 @@
*.log
.DS_Store
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
/.fleet
/.idea
/.nova
/.phpunit.cache
/.vscode
/.zed
/auth.json
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
Homestead.json
Homestead.yaml
Thumbs.db
.npmrc
# Favicons in public (generated by Vite from resources/favicons)
/public/favicon-back
/public/favicon.*
/public/favicon-*.png
/public/apple-touch-icon*.png
/public/apple-icon-*.png
/public/android-chrome-*.png
/public/android-icon-*.png
/public/web-app-manifest-*.png
/public/ms-icon-*.png
/public/site.webmanifest
/public/manifest.json
/docker-compose.yml

View file

@ -1,35 +1,221 @@
.docker-build: stages:
stage: build - install
image: - lint
name: gcr.io/kaniko-project/executor:v1.23.2-debug - test
entrypoint: [ "" ] - security
script: - build
- /kaniko/executor --context "${CI_PROJECT_DIR}" --build-arg "GITLAB_USER=${CI_DEPLOY_USER}" --build-arg "GITLAB_PASSWORD=${CI_DEPLOY_PASSWORD}" --dockerfile "${CI_PROJECT_DIR}/Dockerfile" --destination "${IMAGE_TAG}" --cache=true --cache-ttl 24h - deploy
allow_failure: false
docker-build:staging:
extends: .docker-build
variables:
IMAGE_TAG: "${CI_REGISTRY_IMAGE}:s-${CI_COMMIT_SHA}"
rules:
- if: $CI_COMMIT_BRANCH == "trunk"
when: manual
allow_failure: true
docker-build:latest:
extends: .docker-build
variables:
IMAGE_TAG: "${CI_REGISTRY_IMAGE}:p-${CI_COMMIT_SHA}"
rules:
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master"
when: manual
allow_failure: true
include: include:
- project: 'ldc-web/shared-ci' - project: "ldc-web/shared-ci"
ref: 'main' ref: "main"
file: 'deploy/deploy.yml' file: "security/composer.yml"
- project: "ldc-web/shared-ci"
ref: "main"
file: "build/docker-multistage.yml"
- project: "ldc-web/shared-ci"
ref: "main"
file: "test/test.yml"
- project: "ldc-web/shared-ci"
ref: "main"
file: "deploy/deploy.yml"
stages: # * LINT --------------------------------------------------------------------
- build
- deploy .php-lint-base: &php-lint-base
image: $CI_REGISTRY/ldc-web/projets/ldc-web-images-docker/uwil-ci-8.5:latest
variables:
COMPOSER_CACHE_DIR: ".composer-cache"
CACHE_STORE: "array"
DB_CONNECTION: "sqlite"
DB_DATABASE: ":memory:"
cache:
key:
files:
- composer.lock
prefix: "composer"
policy: pull
paths:
- .composer-cache/
- vendor/
before_script:
- composer config gitlab-token.lab.s-web.fr $CI_DEPLOY_USER $CI_DEPLOY_PASSWORD
- composer install --no-scripts --no-interaction --no-ansi --no-progress --prefer-dist
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "develop"
parallel-lint:
<<: *php-lint-base
stage: lint
script:
- vendor/bin/parallel-lint --exclude vendor --exclude storage --exclude bootstrap --exclude node_modules .
allow_failure: false
pint:
<<: *php-lint-base
stage: lint
script:
- vendor/bin/pint --test
allow_failure: false
phpcs:
<<: *php-lint-base
stage: lint
script:
- vendor/bin/phpcs --standard=phpcs.xml --extensions=php --ignore=vendor/*,storage/*,bootstrap/*,node_modules/* --report=junit --report-file=phpcs-report.xml
artifacts:
when: on_failure
expose_as: 'phpcs'
paths:
- phpcs-report.xml
reports:
junit: phpcs-report.xml
allow_failure: false
phpstan:
<<: *php-lint-base
stage: lint
variables:
PHP_MEMORY_LIMIT: "512M"
COMPOSER_CACHE_DIR: ".composer-cache"
CACHE_STORE: "array"
DB_CONNECTION: "sqlite"
DB_DATABASE: ":memory:"
script:
- php -d memory_limit=512M vendor/bin/phpstan --error-format=junit > phpstan-report.xml
artifacts:
when: on_failure
expose_as: 'phpstan'
paths:
- phpstan-report.xml
reports:
junit: phpstan-report.xml
allow_failure: false
eslint:
stage: lint
image: oven/bun:1
variables:
BUN_INSTALL_CACHE_DIR: ".bun-cache"
cache:
key:
files:
- bun.lock
prefix: "bun"
policy: pull
paths:
- .bun-cache/
before_script:
- echo "@ldc-web:registry=https://${CI_SERVER_HOST}/api/v4/packages/npm/" >> .npmrc
- echo "//${CI_SERVER_HOST}/api/v4/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
- echo "@uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/" >> .npmrc
- echo "//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
- bun install --no-save
script:
- bun run eslint-ci --quiet --output-file=eslint-junit.xml
artifacts:
when: on_failure
expose_as: 'eslint'
paths:
- eslint-junit.xml
reports:
junit: eslint-junit.xml
allow_failure: false
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "develop"
stylelint:
stage: lint
image: oven/bun:1
variables:
BUN_INSTALL_CACHE_DIR: ".bun-cache"
cache:
key:
files:
- bun.lock
prefix: "bun"
policy: pull
paths:
- .bun-cache/
before_script:
- echo "@ldc-web:registry=https://${CI_SERVER_HOST}/api/v4/packages/npm/" >> .npmrc
- echo "//${CI_SERVER_HOST}/api/v4/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
- echo "@uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/" >> .npmrc
- echo "//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
- bun install --no-save
script:
- bun run stylelint-ci --quiet --output-file=stylelint-junit.xml
artifacts:
when: on_failure
expose_as: 'stylelint'
paths:
- stylelint-junit.xml
reports:
junit: stylelint-junit.xml
allow_failure: false
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "develop"
# * TESTS -------------------------------------------------------------------
bun-build-test:
stage: test
image: oven/bun:1
variables:
BUN_INSTALL_CACHE_DIR: ".bun-cache"
cache:
key:
files:
- bun.lock
prefix: "bun"
policy: pull
paths:
- .bun-cache/
before_script:
- echo "@ldc-web:registry=https://${CI_SERVER_HOST}/api/v4/packages/npm/" >> .npmrc
- echo "//${CI_SERVER_HOST}/api/v4/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
- echo "@uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/" >> .npmrc
- echo "//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
- bun install --no-save
script:
- timeout 300 bun run prod
artifacts:
expose_as: 'front-build-test'
paths:
- public/build/manifest.json
allow_failure: false
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "trunk" || $CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "main"
- if: $CI_COMMIT_TAG != null
# * SECURITY ----------------------------------------------------------------
npm-security:
stage: security
image: oven/bun:1
before_script:
- echo "@ldc-web:registry=https://${CI_SERVER_HOST}/api/v4/packages/npm/" >> .npmrc
- echo "//${CI_SERVER_HOST}/api/v4/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
- echo "@uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/" >> .npmrc
- echo "//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
script:
- bun audit --json > bun-security-checker-report.json
artifacts:
when: on_failure
expose_as: 'bun-security-checker'
paths:
- bun-security-checker-report.json
allow_failure: true
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "trunk" || $CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "main"
- if: $CI_COMMIT_TAG != null
# * BUILD -------------------------------------------------------------------
.docker-build:
dependencies: []

View file

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

View file

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

2
.husky/commit-msg Executable file
View file

@ -0,0 +1,2 @@
#!/bin/sh
npx commitlint --config node_modules/@uwil/husky-config/commitlint.config.js --edit $1

56
.stylelintrc.json Normal file
View file

@ -0,0 +1,56 @@
{
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-sass-guidelines",
"stylelint-config-idiomatic-order"
],
"plugins": ["stylelint-scss"],
"files": ["**/*.css", "**/*.scss"],
"ignoreFiles": [
"**/vendor/*.css",
"**/vendor/**/*.css",
"**/vendor/*.scss",
"**/vendor/**/*.scss",
"**/vendor/*.sass",
"**/vendor/**/*.sass",
"**/packages/*.css",
"**/packages/**/*.css",
"**/packages/*.scss",
"**/packages/**/*.scss",
"**/packages/*.sass",
"**/packages/**/*.sass"
],
"customSyntax": "postcss-scss",
"rules": {
"declaration-property-value-no-unknown": true,
"selector-max-id": null,
"color-no-invalid-hex": true,
"order/properties-alphabetical-order": null,
"max-nesting-depth": 5,
"selector-max-compound-selectors": 5,
"scss/double-slash-comment-empty-line-before": [
"always",
{
"ignore": [
"between-comments",
"stylelint-commands",
"inside-block"
]
}
],
"at-rule-no-unknown": null,
"scss/at-rule-no-unknown": [
true,
{
"ignoreAtRules": [
"tailwind",
"apply",
"variants",
"responsive",
"screen",
"layer"
]
}
]
}
}

5
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"recommendations": [
"emeraldwalk.runonsave"
]
}

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

@ -0,0 +1,109 @@
{
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#b0981c",
"activityBar.activeBorder": "#15202b",
"activityBar.background": "#b0981c",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#91edde",
"activityBarBadge.foreground": "#15202b",
"sash.hoverBorder": "#b0981c",
"statusBar.background": "#847215",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#b0981c",
"statusBarItem.remoteBackground": "#1fc0a6",
"statusBarItem.remoteForeground": "#e7e7e7",
"titleBar.activeBackground": "#847215",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#84721599",
"titleBar.inactiveForeground": "#e7e7e799",
"commandCenter.border": "#e7e7e799",
"activityBarTop.activeBackground": "#b0981c",
"activityBarTop.activeBorder": "#15202b",
"activityBarTop.background": "#b0981c",
"activityBarTop.foreground": "#15202b",
"activityBarTop.inactiveForeground": "#15202b99",
"commandCenter.foreground": "#e7e7e7",
"statusBar.debuggingBackground": "#847215",
"statusBar.debuggingForeground": "#e7e7e7",
"statusBar.inactiveBackground": "#847215"
},
"peacock.color": "#847215",
"peacock.remoteColor": "#847215",
"intelephense.environment.phpVersion": "8.5.0",
"intelephense.environment.includePaths": [
"./.intellipest",
"./vendor/fakerphp/faker",
"./vendor/pestphp",
"./vendor/phpunit/phpunit",
"./vendor/laravel"
],
"phpstan.binCommand": [
"${workspaceFolder}/vendor/bin/phpstan"
],
"laravel-pint.enable": true,
"editor.formatOnSave": true,
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "html", "vue"],
"[html]": {
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "always"
}
},
"[blade]": {
"editor.formatOnSave": true,
"editor.formatOnType": false
},
"[javascript]": {
"editor.autoIndent": "full",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "always"
},
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.detectIndentation": false,
"editor.formatOnSave": true
},
"[scss]": {
"editor.defaultFormatter": "stylelint.vscode-stylelint",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": "always"
}
},
"[typescript]": {
"editor.autoIndent": "full",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "always"
},
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.detectIndentation": false,
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file"
},
"[php]": {
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file"
},
"[vue]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "always"
}
},
"conventionalCommits.scopes": [
"migration"
],
"emeraldwalk.runonsave": {
"autoClearConsole": false,
"commands": [
{
"match": "\\.php$",
"isAsync": true,
"cmd": "${workspaceFolder}/vendor/bin/pint ${file}"
}
]
},
"LaravelExtraIntellisense.phpCommand": "docker exec -w /app/srcs -t ovoteam-$(whoami)-apache-php-1 php -r \"{code}\"",
"LaravelExtraIntellisense.basePathForCode": "/app/srcs"
}

12
CHANGELOG.md Normal file
View file

@ -0,0 +1,12 @@
# Changelog
All notable changes to `laravel-project` will be documented in this file.
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
- Commits respect [Conventionnal commits](https://www.conventionalcommits.org/en/v1.0.0/) & use [Gitmoji](https://gitmoji.dev/).
## **[v1.0.0] - 01/01/2026**
### Added
- feat: 🎉 working project

View file

@ -1,4 +1,55 @@
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/ldcweb-production-8.3:170625-101047 ARG PHP_8_5_TAG=latest
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/uwil-ci-8.5:latest AS vendor
WORKDIR /app
ARG GITLAB_USER
ARG GITLAB_PASSWORD
COPY database/ database/
COPY composer.json composer.json
COPY composer.lock composer.lock
RUN composer config gitlab-token.lab.s-web.fr "${GITLAB_USER}" "${GITLAB_PASSWORD}"
RUN composer install \
--ignore-platform-reqs \
--no-interaction \
--no-plugins \
--no-scripts \
--no-dev \
--prefer-dist
FROM oven/bun:1.4.4-alpine AS frontend
ARG GITLAB_PASSWORD
RUN mkdir -p /app/public/build
WORKDIR /app
COPY package.json bun.lock* postcss.config.js tailwind.config.js tsconfig.json vite.config.ts .eslintrc.cjs .stylelintrc.json /app/
COPY resources/ /app/resources/
COPY --from=vendor /app/vendor/ /app/vendor/
RUN echo "@ldc-web:registry=https://lab.s-web.fr/api/v4/packages/npm/" >> .npmrc && \
echo "//lab.s-web.fr/api/v4/packages/npm/:_authToken=${GITLAB_PASSWORD}" >> .npmrc && \
echo "@uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/" >> .npmrc && \
echo "//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken=${GITLAB_PASSWORD}" >> .npmrc
RUN bun install --frozen-lockfile && bun run prod
ARG PHP_8_5_TAG
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/uwil-production-8.5:${PHP_8_5_TAG}
ARG USER=www-data ARG USER=www-data
COPY --chown=${USER}:${USER} . /app/srcs COPY --chown=${USER}:${USER} . /app/srcs
COPY --chown=${USER}:${USER} ./resources/favicons/front/ /app/srcs/public/
COPY --chown=${USER}:${USER} ./resources/favicons/back/ /app/srcs/public/back/
COPY --from=vendor --chown=${USER}:${USER} /app/vendor/ /app/srcs/vendor/
COPY --from=frontend --chown=${USER}:${USER} /app/public/build/ /app/srcs/public/build/
USER root
RUN chmod -R 755 /app/srcs/storage
USER ${USER} USER ${USER}

122
README.md
View file

@ -1,111 +1,33 @@
# OVO TEAM # Table of contents
## Instalation - [Table of contents](#table-of-contents)
- [Frameworks, Platforms and Libraries](#frameworks-platforms-and-libraries)
- [Documentation](#documentation)
- [Changelog](#changelog)
- [Copyright and License](#copyright-and-license)
Copier `.env` vers `.env.example` et remplir les informations. # Frameworks, Platforms and Libraries
## Déployer <!-- https://github.com/Ileriayo/markdown-badges -->
- Apres git pull sur l'envirronnement désiré [![SASS](https://img.shields.io/badge/SASS-hotpink.svg?style=for-the-badge&logo=SASS&logoColor=white)](https://sass-lang.com/)
- Envoyer les éléments uploads si nécéssaire (Ne devrais pas étre le cas) [![TailwindCSS](https://img.shields.io/badge/tailwindcss-%2338B2AC.svg?style=for-the-badge&logo=tailwind-css&logoColor=white)](https://tailwindcss.com/)
- Executer la partie `Compiler le front` [![Vite](https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge&logo=vite&logoColor=white)](https://vite.dev/)
[![PHP](https://img.shields.io/badge/php-%23777BB4.svg?style=for-the-badge&logo=php&logoColor=white)](https://www.php.net/)
[![Laravel](https://img.shields.io/badge/laravel-%23FF2D20.svg?style=for-the-badge&logo=laravel&logoColor=white)](https://laravel.com/docs/master)
## Faire une mise à jour # Documentation
Commenter dans `public/.htaccess` la partie `# Hide update.php`, puis accéder à ce fichier via la navigateur ```php
composer i/npm i
```
## Fichier uploads # Changelog
Au sein de `public/sites/default/files` Latest version v1.0.0.
Les dossiers : See the [CHANGELOG.md](CHANGELOG.md) file for details.
- `inline-images` Pour les contenus insérés dans les WYSIWYG # Copyright and License
- les dossiers `20*` comme `2023-05` par exemple pour les media uploadés via la création de pages
- `styles` qui contienent des images uploadés de dimension spécifiques
## Compiler le front Copyright (c) 2025 and beyond UWIL.
`node_modules/gulp/bin/gulp.js -f public/themes/custom/custtheme/gulpfile.js`
Deactiver `Aggreger les fichiers CSS et javascript` au sein de `/fr/admin/config/development/performance`
Cliquer sur `Effacer tous les caches` si vous reactivez l'option
---
## Original README
Drupal is an open source content management platform supporting a variety of
websites ranging from personal weblogs to large community-driven websites. For
more information, visit the Drupal website, [Drupal.org][Drupal.org], and join
the [Drupal community][Drupal community].
## Contributing
Drupal is developed on [Drupal.org][Drupal.org], the home of the international
Drupal community since 2001!
[Drupal.org][Drupal.org] hosts Drupal's [GitLab repository][GitLab repository],
its [issue queue][issue queue], and its [documentation][documentation]. Before
you start working on code, be sure to search the [issue queue][issue queue] and
create an issue if your aren't able to find an existing issue.
Every issue on Drupal.org automatically creates a new community-accessible fork
that you can contribute to. Learn more about the code contribution process on
the [Issue forks & merge requests page][issue forks].
## Usage
For a brief introduction, see [USAGE.txt](/core/USAGE.txt). You can also find
guides, API references, and more by visiting Drupal's [documentation
page][documentation].
You can quickly extend Drupal's core feature set by installing any of its
[thousands of free and open source modules][modules]. With Drupal and its
module ecosystem, you can often build most or all of what your project needs
before writing a single line of code.
## Changelog
Drupal keeps detailed [change records][changelog]. You can search Drupal's
changes for a record of every notable breaking change and new feature since
2011.
## Security
For a list of security announcements, see the [Security advisories
page][Security advisories] (available as [an RSS feed][security RSS]). This
page also describes how to subscribe to these announcements via email.
For information about the Drupal security process, or to find out how to report
a potential security issue to the Drupal security team, see the [Security team
page][security team].
## Need a helping hand?
Visit the [Support page][support] or browse [over a thousand Drupal
providers][service providers] offering design, strategy, development, and
hosting services.
## Legal matters
Know your rights when using Drupal by reading Drupal core's
[license](/core/LICENSE.txt).
Learn about the [Drupal trademark and logo policy here][trademark].
[Drupal.org]: https://www.drupal.org
[Drupal community]: https://www.drupal.org/community
[GitLab repository]: https://git.drupalcode.org/project/drupal
[issue queue]: https://www.drupal.org/project/issues/drupal
[issue forks]: https://www.drupal.org/drupalorg/docs/gitlab-integration/issue-forks-merge-requests
[documentation]: https://www.drupal.org/documentation
[changelog]: https://www.drupal.org/list-changes/drupal
[modules]: https://www.drupal.org/project/project_module
[security advisories]: https://www.drupal.org/security
[security RSS]: https://www.drupal.org/security/rss.xml
[security team]: https://www.drupal.org/drupal-security-team
[service providers]: https://www.drupal.org/drupal-services
[support]: https://www.drupal.org/support
[trademark]: https://www.drupal.com/trademark

32
RGPD.md Normal file
View file

@ -0,0 +1,32 @@
# RGPD
## Site
### En base de données
| Table | Colonne | Contenu | Utilisation |
| ----- | ------- | ------- | ----------- |
| activity_logs | user_id | Identifiant de l'utilisateur (back) | Tracer les modifications dans le BO |
| --- | --- | --- | --- |
| users | first_name | Prénom | Affichage à différents endroits du BO |
| users | last_name | Nom | Affichage à différents endroits du BO |
| users | email | Adresse mail | Identifiant, sert à la connexion au BO |
### Formulaire de contact (marketing)
| Donnée | Contenu |
| ------ | ------- |
| Nom | Nom de la personne |
| Prénom | Prénom de la personne |
| Téléphone | Numéro de téléphone de la personne |
| Adresse E-mail | Adresse email de la personne |
| Nom de l'entreprise | Nom de l'entreprise de la personne |
| Secteur d'activité | Secteur d'activité de la personne |
## Cookies
| Service | Utilité |
| ------- | ------- |
| Matomo | Mesure d'audience |
| Google Recaptcha | Empêcher la soumission de formulaire par des robots |
| Youtube | Visionner du contenu disponible sur la plateforme Youtube |

31184
_ide_helper.php Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
/** @implements CastsAttributes<StorageFilePath|null, StorageFilePath|string|null> */
class FilePathCast implements CastsAttributes
{
/** @param array<string, mixed> $attributes */
public function get(Model $model, string $key, mixed $value, array $attributes): ?StorageFilePath
{
if ($value === null) {
return null;
}
if (! is_string($value)) {
throw new \InvalidArgumentException('The given value is not a valid file path string.');
}
return new StorageFilePath($value);
}
/** @param array<string, mixed> $attributes */
public function set(Model $model, string $key, mixed $value, array $attributes): ?string
{
if ($value === null) {
return null;
}
if ($value instanceof StorageFilePath) {
return $value->value;
}
if (is_string($value)) {
return (new StorageFilePath($value))->value;
}
throw new \InvalidArgumentException('The given value is not a valid StorageFilePath instance or path string.');
}
}

View file

@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace App\Casts;
use Domain\Models\ValueObjects\Products\NutritionalValue;
use Domain\Models\ValueObjects\Products\NutritionalValues;
use Domain\Models\ValueObjects\Products\NutritionnalUnit;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
/**
* @implements CastsAttributes<NutritionalValues|null, NutritionalValues|array<string, mixed>|string|null>
*/
class NutritionalValuesCast implements CastsAttributes
{
/**
* @param array<string, mixed> $attributes
*/
public function get(Model $model, string $key, mixed $value, array $attributes): ?NutritionalValues
{
if ($value === null || $value === '' || $value === '[]') {
return null;
}
if (! is_string($value)) {
throw new \InvalidArgumentException('The given value is not a string.');
}
$data = json_decode($value, true);
if (! is_array($data)) {
throw new \InvalidArgumentException('The given value is not a valid JSON array.');
}
if ($data === []) {
return null;
}
return $this->fromArray($data);
}
/**
* @param array<string, mixed> $attributes
* @return array<string, string>
*/
public function set(Model $model, string $key, mixed $value, array $attributes): array
{
if ($value === null || $value === [] || $value === '' || $value === '[]') {
return [$key => '[]'];
}
if (is_string($value)) {
$value = json_decode($value, true);
}
if (is_array($value)) {
$value = $this->fromArray($value);
}
if ($value === null) {
return [$key => '[]'];
}
if (! $value instanceof NutritionalValues) {
throw new \InvalidArgumentException('The given value is not a NutritionalValues instance.');
}
return [$key => (string) json_encode($value->toArray(), JSON_THROW_ON_ERROR)];
}
/**
* @param array<array-key, mixed> $data
*/
private function fromArray(array $data): ?NutritionalValues
{
if ($data === []) {
return null;
}
$getProperty = function (string $key) use ($data): ?NutritionalValue {
$item = Arr::get($data, $key);
return is_array($item) ? $this->toNutritionalValue($item) : null;
};
$refWeightG = $getProperty('refWeightG');
if ($refWeightG === null) {
return null;
}
return new NutritionalValues(
refWeightG: $refWeightG,
kj: $getProperty('kj'),
kcal: $getProperty('kcal'),
fatG: $getProperty('fatG'),
satFatG: $getProperty('satFatG'),
carbG: $getProperty('carbG'),
sugarG: $getProperty('sugarG'),
fiberG: $getProperty('fiberG'),
proteinG: $getProperty('proteinG'),
saltG: $getProperty('saltG'),
);
}
/**
* @param array<array-key, mixed> $data
*/
private function toNutritionalValue(array $data): ?NutritionalValue
{
$rawItemVal = Arr::get($data, 'value');
$rawItemUnit = Arr::get($data, 'unit');
if (! is_string($rawItemVal) && ! is_numeric($rawItemVal)) {
return null;
}
if (! is_string($rawItemUnit) && ! $rawItemUnit instanceof NutritionnalUnit) {
return null;
}
$value = mb_trim((string) $rawItemVal);
$unit = $rawItemUnit instanceof NutritionnalUnit
? $rawItemUnit
: NutritionnalUnit::tryFrom((string) $rawItemUnit);
if ($value === '' || $unit === null) {
return null;
}
return new NutritionalValue($value, $unit);
}
}

18
app/Contracts/Action.php Normal file
View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Contracts;
/**
* @template TInput of \App\Contracts\ActionInput
* @template TResult
*/
interface Action
{
/**
* @param TInput $input
* @return TResult
*/
public function __invoke(ActionInput $input);
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Contracts;
interface ActionInput
{
}

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Contracts;
final class ActionInputEmpty implements ActionInput
{
public static function create(): self
{
return new self();
}
}

View file

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Directories;
use App\Contracts\Directory;
use App\Dto\CategoryProductsDto;
use Domain\Models\Entities\Product;
use Domain\Models\ValueObjects\Entities\ProductId;
use Domain\Models\ValueObjects\Entities\RecipeId;
use Domain\Models\ValueObjects\Pagination;
use Domain\Models\ValueObjects\Products\ProductCategory;
use Illuminate\Pagination\LengthAwarePaginator;
/** @extends Directory<int, \Domain\Models\Entities\Product> */
interface ProductDirectory extends Directory
{
public function find(ProductId $id): ?Product;
public function findBySlug(string $slug): ?Product;
/**
* @return \Domain\Models\ValueObjects\Products\ProductSummary[]
*/
public function getAllProductsSummaries(): array;
/**
* @return LengthAwarePaginator<array-key,Product>
*/
public function getPaginatedProducts(
Pagination $pagination,
string $search = '',
): LengthAwarePaginator;
/**
* @param \Domain\Models\ValueObjects\Products\BreedingMethod[] $breedingMethods
* @param \Domain\Models\ValueObjects\Products\Technology[] $technologies
* @param \Domain\Models\ValueObjects\Products\Business[] $businesses
*/
public function getCategoryProducts(
ProductCategory $category,
array $breedingMethods = [],
array $technologies = [],
array $businesses = [],
?bool $isEgalim = null,
string $search = '',
int $limitPerCategory = 6
): ?CategoryProductsDto;
/** @return \Domain\Models\Entities\Product[] */
public function getRelatedProducts(RecipeId $recipeId, int $limit = 4): array;
}

View file

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Directories;
use App\Contracts\Directory;
use Domain\Models\Entities\Recipe;
use Domain\Models\ValueObjects\Entities\ProductId;
use Domain\Models\ValueObjects\Entities\RecipeId;
use Domain\Models\ValueObjects\Pagination;
use Illuminate\Pagination\LengthAwarePaginator;
/** @extends Directory<int, \Domain\Models\Entities\Recipe> */
interface RecipeDirectory extends Directory
{
public function find(RecipeId $id): ?Recipe;
public function findBySlug(string $slug): ?Recipe;
/**
* @param \Domain\Models\ValueObjects\Products\ProductCategory[] $productRanges
* @param \Domain\Models\ValueObjects\Recipes\ConsumptionTime[] $consumptionTimes
* @param \Domain\Models\ValueObjects\Recipes\Inspiration[] $inspirations
* @param \Domain\Models\ValueObjects\Recipes\Season[] $seasons
* @return LengthAwarePaginator<array-key,Recipe>
*/
public function getPaginatedRecipes(
Pagination $pagination,
array $productRanges = [],
array $consumptionTimes = [],
array $inspirations = [],
array $seasons = [],
string $search = ''
): LengthAwarePaginator;
/** @return \Domain\Models\Entities\Recipe[] */
public function getRelatedRecipes(ProductId $productId, int $limit = 4): array;
}

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Contracts;
/**
* @template TEntity
* @template TId
*
* @method TEntity|null find(TId $id)
*/
interface Directory
{
}

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Contracts;
interface Executor
{
/**
* @template TInputAction of \App\Contracts\ActionInput
* @template TInputQuery of \App\Contracts\QueryInput
* @template TResult
*
* @param \App\Contracts\Query<TInputQuery,TResult>|\App\Contracts\Action<TInputAction,TResult> $executable
* @param TInputAction|TInputQuery $input
* @return TResult
*/
public function execute(Action | Query $executable, ActionInput | QueryInput $input): mixed;
}

18
app/Contracts/Query.php Normal file
View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Contracts;
/**
* @template TInput of \App\Contracts\QueryInput
* @template TResult
*/
interface Query
{
/**
* @param TInput $input
* @return TResult
*/
public function __invoke(QueryInput $input);
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Contracts;
interface QueryInput
{
}

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Contracts;
final class QueryInputEmpty implements QueryInput
{
public static function create(): self
{
return new self();
}
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Contracts;
/**
* @template TModel of \Illuminate\Database\Eloquent\Model
* @template TId of \Ramsey\Uuid\UuidInterface|int
*
* @method TModel|null find(TId $id)
* @method void persist(TModel $model)
* @method void delete(TModel $model)
*/
interface Repository
{
}

View file

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\Create;
use App\Contracts\Action;
use App\Contracts\ActionInput;
use App\Models\Product;
use Domain\Models\ValueObjects\Entities\ProductId;
use Ramsey\Uuid\Uuid;
use Webmozart\Assert\Assert;
/**
* phpcs:disable Generic.Files.LineLength.TooLong
*
* @implements \App\Contracts\Action<\App\Cqs\Actions\Products\Create\CreateProductInput, \Domain\Models\ValueObjects\Entities\ProductId|\App\Cqs\Actions\Products\Create\FailedToCreateProduct>
* phpcs:enable
*/
final class CreateProduct implements Action
{
/**
* @param \App\Cqs\Actions\Products\Create\CreateProductInput $input
*/
public function __invoke(ActionInput $input): ProductId | FailedToCreateProduct
{
Assert::isInstanceOf($input, CreateProductInput::class);
$product = new Product([
'seo_title' => $input->seoTitle,
'seo_description' => $input->seoDescription,
'name' => $input->name,
'picture' => $input->picture,
'picture_alt' => $input->pictureAlt,
'picture_title' => $input->pictureTitle,
'category' => $input->category,
'technology' => $input->technology,
'is_egalim' => $input->isEgalim,
'is_published' => $input->isPublished,
'breeding_methods' => $input->breedingMethods,
'businesses' => $input->businesses,
'formats' => $input->formats,
'description' => $input->description,
'details_and_advantages' => $input->detailsAndAdvantages,
'handling_advices' => $input->handlingAdvices,
'ingredients' => $input->ingredients,
'conservation_before_opening' => $input->conservationBeforeOpening,
'conservation_after_opening' => $input->conservationAfterOpening,
'nutritional_values' => $input->nutritionalValues,
'pdf' => $input->pdf,
]);
if (! $product->save()) {
return new FailedToCreateProduct();
}
return new ProductId(Uuid::fromString($product->id));
}
}

View file

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\Create;
use App\Contracts\ActionInput;
use Domain\Models\ValueObjects\Products\BreedingMethod;
use Domain\Models\ValueObjects\Products\Business;
use Domain\Models\ValueObjects\Products\NutritionnalUnit;
use Domain\Models\ValueObjects\Products\ProductCategory;
use Domain\Models\ValueObjects\Products\ProductFormat;
use Domain\Models\ValueObjects\Products\Technology;
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
use Webmozart\Assert\Assert;
final readonly class CreateProductInput implements ActionInput
{
/**
* @param BreedingMethod[] $breedingMethods
* @param Business[] $businesses
* @param ProductFormat[] $formats
* @param array<string, array{unit: NutritionnalUnit, value: string}> $nutritionalValues
*/
public function __construct(
public string $name,
public string $seoTitle,
public string $seoDescription,
public ProductCategory $category,
public Technology $technology,
public bool $isEgalim,
public bool $isPublished,
public array $breedingMethods,
public array $businesses,
public array $formats,
public string $description,
public string $detailsAndAdvantages,
public string $handlingAdvices,
public ?string $ingredients,
public string $conservationBeforeOpening,
public ?string $conservationAfterOpening,
public ?StorageFilePath $picture,
public string $pictureAlt,
public string $pictureTitle,
public ?StorageFilePath $pdf,
public array $nutritionalValues,
) {
Assert::notEmpty($this->name);
Assert::notEmpty($this->seoTitle);
Assert::notEmpty($this->seoDescription);
Assert::notEmpty($this->description);
Assert::notEmpty($this->detailsAndAdvantages);
Assert::notEmpty($this->handlingAdvices);
Assert::notEmpty($this->conservationBeforeOpening);
Assert::notEmpty($this->pictureAlt);
Assert::notEmpty($this->pictureTitle);
Assert::allIsAOf($this->breedingMethods, BreedingMethod::class);
Assert::allIsAOf($this->businesses, Business::class);
Assert::allIsAOf($this->formats, ProductFormat::class);
Assert::notEmpty($this->breedingMethods);
Assert::notEmpty($this->businesses);
Assert::notEmpty($this->formats);
when($this->ingredients !== null, fn () => Assert::notEmpty($this->ingredients));
when($this->conservationAfterOpening !== null, fn () => Assert::notEmpty($this->conservationAfterOpening));
foreach ($this->nutritionalValues as $value) {
Assert::isArray($value);
Assert::keyExists($value, 'unit');
Assert::keyExists($value, 'value');
Assert::isInstanceOf($value['unit'], NutritionnalUnit::class);
Assert::string($value['value']);
}
}
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\Create;
final readonly class FailedToCreateProduct
{
}

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\Delete;
use App\Contracts\Action;
use App\Contracts\ActionInput;
use App\Models\Product;
use Domain\Models\ValueObjects\Entities\ProductId;
use Ramsey\Uuid\Uuid;
use Webmozart\Assert\Assert;
/**
* phpcs:disable Generic.Files.LineLength.TooLong
*
* @implements \App\Contracts\Action<\App\Cqs\Actions\Products\Delete\DeleteProductInput, \Domain\Models\ValueObjects\Entities\ProductId|\App\Cqs\Actions\Products\Delete\FailedToDeleteProduct>
* phpcs:enable
*/
final class DeleteProduct implements Action
{
/**
* @param \App\Cqs\Actions\Products\Delete\DeleteProductInput $input
*/
public function __invoke(ActionInput $input): ProductId | FailedToDeleteProduct
{
Assert::isInstanceOf($input, DeleteProductInput::class);
$product = Product::query()->findOrFail((string) $input->id->value);
if (! $product->delete()) {
return new FailedToDeleteProduct();
}
return new ProductId(Uuid::fromString($product->id));
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\Delete;
use App\Contracts\ActionInput;
use Domain\Models\ValueObjects\Entities\ProductId;
final readonly class DeleteProductInput implements ActionInput
{
public function __construct(
public ProductId $id,
) {
}
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\Delete;
final readonly class FailedToDeleteProduct
{
}

View file

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\Duplicate;
use App\Contracts\Action;
use App\Contracts\ActionInput;
use App\Lib\PublicS3Disk;
use App\Models\Product;
use Domain\Models\ValueObjects\Entities\ProductId;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Ramsey\Uuid\Uuid;
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
use Webmozart\Assert\Assert;
/**
* phpcs:disable Generic.Files.LineLength.TooLong
*
* @implements \App\Contracts\Action<\App\Cqs\Actions\Products\Duplicate\DuplicateProductInput, \Domain\Models\ValueObjects\Entities\ProductId|\App\Cqs\Actions\Products\Duplicate\FailedToDuplicateProduct>
* phpcs:enable
*/
final class DuplicateProduct implements Action
{
public function __construct(
private readonly PublicS3Disk $s3Disk,
) {
}
/**
* @param \App\Cqs\Actions\Products\Duplicate\DuplicateProductInput $input
*/
public function __invoke(ActionInput $input): ProductId | FailedToDuplicateProduct
{
Assert::isInstanceOf($input, DuplicateProductInput::class);
$original = Product::withTrashed()->findOrFail((string) $input->productId->value);
$clone = $original->replicate();
$clone->name = $this->generateUniqueName($original->name);
$clone->slug = Str::slug($clone->name);
$duplicatedPicture = $this->duplicateFile($original->picture);
if ($duplicatedPicture !== null) {
$clone->picture = $duplicatedPicture;
}
$clone->pdf = $this->duplicateFile($original->pdf);
$clone->is_published = false;
if (! $clone->save()) {
return new FailedToDuplicateProduct();
}
return new ProductId(Uuid::fromString($clone->id));
}
private function generateUniqueName(string $originalName): string
{
$rootName = $originalName;
if (preg_match('/^(.*?)(?: \(Copie(?: (\d+))?\))?$/', $originalName, $matches)) {
$rootName = $matches[1];
}
$baseName = "{$rootName} (Copie)";
if (
! Product::withTrashed()
->where('name', $baseName)
->orWhere('slug', Str::slug($baseName))
->exists()
) {
return $baseName;
}
$i = 2;
while (
Product::withTrashed()
->where('name', "{$rootName} (Copie {$i})")
->orWhere('slug', Str::slug("{$rootName} (Copie {$i})"))
->exists()
) {
$i++;
}
return "{$rootName} (Copie {$i})";
}
private function duplicateFile(?StorageFilePath $filePath): ?StorageFilePath
{
if ($filePath === null) {
return null;
}
$sourcePath = $filePath->value;
$disk = Storage::disk($this->s3Disk->disk->value);
if (! $disk->exists($sourcePath)) {
return null;
}
$extension = pathinfo($sourcePath, PATHINFO_EXTENSION);
$dirname = pathinfo($sourcePath, PATHINFO_DIRNAME);
$newFilename = Str::uuid()->toString() . ($extension !== '' ? '.' . $extension : '');
$destinationPath = ($dirname !== '.' && $dirname !== '') ? "{$dirname}/{$newFilename}" : $newFilename;
if (! $disk->copy($sourcePath, $destinationPath)) {
return null;
}
return new StorageFilePath($destinationPath);
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\Duplicate;
use App\Contracts\ActionInput;
use Domain\Models\ValueObjects\Entities\ProductId;
final readonly class DuplicateProductInput implements ActionInput
{
public function __construct(
public ProductId $productId,
) {
}
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\Duplicate;
final readonly class FailedToDuplicateProduct
{
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\ForceDelete;
final readonly class FailedToForceDeleteProduct
{
}

View file

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\ForceDelete;
use App\Contracts\Action;
use App\Contracts\ActionInput;
use App\Lib\PublicS3Disk;
use App\Models\Product;
use Domain\Models\ValueObjects\Entities\ProductId;
use Ramsey\Uuid\Uuid;
use Uwil\Storage\Contracts\StorageService;
use Webmozart\Assert\Assert;
/**
* phpcs:disable Generic.Files.LineLength.TooLong
*
* @implements \App\Contracts\Action<\App\Cqs\Actions\Products\ForceDelete\ForceDeleteProductInput, \Domain\Models\ValueObjects\Entities\ProductId|\App\Cqs\Actions\Products\ForceDelete\FailedToForceDeleteProduct>
* phpcs:enable
*/
final class ForceDeleteProduct implements Action
{
public function __construct(
private readonly StorageService $storageService,
private readonly PublicS3Disk $s3Disk,
) {
}
/**
* @param \App\Cqs\Actions\Products\ForceDelete\ForceDeleteProductInput $input
*/
public function __invoke(ActionInput $input): ProductId | FailedToForceDeleteProduct
{
Assert::isInstanceOf($input, ForceDeleteProductInput::class);
$product = Product::withTrashed()->findOrFail((string) $input->id->value);
$picture = $product->picture;
$pdf = $product->pdf;
if (! $product->forceDelete()) {
return new FailedToForceDeleteProduct();
}
if ($picture !== null) {
$this->storageService->removeFromDisk($picture, $this->s3Disk->disk);
}
if ($pdf !== null) {
$this->storageService->removeFromDisk($pdf, $this->s3Disk->disk);
}
return new ProductId(Uuid::fromString($product->id));
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\ForceDelete;
use App\Contracts\ActionInput;
use Domain\Models\ValueObjects\Entities\ProductId;
final readonly class ForceDeleteProductInput implements ActionInput
{
public function __construct(
public ProductId $id,
) {
}
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\Restore;
final readonly class FailedToRestoreProduct
{
}

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\Restore;
use App\Contracts\Action;
use App\Contracts\ActionInput;
use App\Models\Product;
use Domain\Models\ValueObjects\Entities\ProductId;
use Ramsey\Uuid\Uuid;
use Webmozart\Assert\Assert;
/**
* phpcs:disable Generic.Files.LineLength.TooLong
*
* @implements \App\Contracts\Action<\App\Cqs\Actions\Products\Restore\RestoreProductInput, \Domain\Models\ValueObjects\Entities\ProductId|\App\Cqs\Actions\Products\Restore\FailedToRestoreProduct>
* phpcs:enable
*/
final class RestoreProduct implements Action
{
/**
* @param \App\Cqs\Actions\Products\Restore\RestoreProductInput $input
*/
public function __invoke(ActionInput $input): ProductId | FailedToRestoreProduct
{
Assert::isInstanceOf($input, RestoreProductInput::class);
$product = Product::withTrashed()->findOrFail((string) $input->id->value);
if (! $product->restore()) {
return new FailedToRestoreProduct();
}
return new ProductId(Uuid::fromString($product->id));
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\Restore;
use App\Contracts\ActionInput;
use Domain\Models\ValueObjects\Entities\ProductId;
final readonly class RestoreProductInput implements ActionInput
{
public function __construct(
public ProductId $id,
) {
}
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\Update;
final readonly class FailedToUpdateProduct
{
}

View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\Update;
use App\Contracts\Action;
use App\Contracts\ActionInput;
use App\Dto\ProductDto;
use App\Lib\PublicS3Disk;
use App\Models\Product;
use Uwil\Storage\Contracts\StorageService;
use Webmozart\Assert\Assert;
/**
* phpcs:disable Generic.Files.LineLength.TooLong
*
* @implements \App\Contracts\Action<\App\Cqs\Actions\Products\Update\UpdateProductInput, \App\Dto\ProductDto|\App\Cqs\Actions\Products\Update\FailedToUpdateProduct>
* phpcs:enable
*/
final class UpdateProduct implements Action
{
public function __construct(
private readonly StorageService $storageService,
private readonly PublicS3Disk $s3Disk,
) {
}
/**
* @param \App\Cqs\Actions\Products\Update\UpdateProductInput $input
*/
public function __invoke(ActionInput $input): ProductDto | FailedToUpdateProduct
{
Assert::isInstanceOf($input, UpdateProductInput::class);
$product = Product::query()->findOrFail((string) $input->id->value);
$oldPicture = $product->picture;
$oldPdf = $product->pdf;
$product->fill([
'seo_title' => $input->seoTitle,
'seo_description' => $input->seoDescription,
'name' => $input->name,
'picture_alt' => $input->pictureAlt,
'picture_title' => $input->pictureTitle,
'category' => $input->category,
'technology' => $input->technology,
'is_egalim' => $input->isEgalim,
'is_published' => $input->isPublished,
'breeding_methods' => $input->breedingMethods,
'businesses' => $input->businesses,
'formats' => $input->formats,
'description' => $input->description,
'details_and_advantages' => $input->detailsAndAdvantages,
'handling_advices' => $input->handlingAdvices,
'ingredients' => $input->ingredients,
'conservation_before_opening' => $input->conservationBeforeOpening,
'conservation_after_opening' => $input->conservationAfterOpening,
'nutritional_values' => $input->nutritionalValues,
]);
if ($input->picture !== null) {
$product->picture = $input->picture;
}
$product->pdf = $input->pdf;
if (! $product->save()) {
return new FailedToUpdateProduct();
}
// Clean up replaced or removed S3 files
if ($input->picture !== null && $oldPicture !== null && $oldPicture->value !== $input->picture->value) {
$this->storageService->removeFromDisk($oldPicture, $this->s3Disk->disk);
}
if ($oldPdf !== null && ($input->pdf === null || $oldPdf->value !== $input->pdf->value)) {
$this->storageService->removeFromDisk($oldPdf, $this->s3Disk->disk);
}
return ProductDto::fromModel($product, $this->s3Disk);
}
}

View file

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Products\Update;
use App\Contracts\ActionInput;
use Domain\Models\ValueObjects\Entities\ProductId;
use Domain\Models\ValueObjects\Products\BreedingMethod;
use Domain\Models\ValueObjects\Products\Business;
use Domain\Models\ValueObjects\Products\NutritionnalUnit;
use Domain\Models\ValueObjects\Products\ProductCategory;
use Domain\Models\ValueObjects\Products\ProductFormat;
use Domain\Models\ValueObjects\Products\Technology;
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
use Webmozart\Assert\Assert;
final readonly class UpdateProductInput implements ActionInput
{
/**
* @param BreedingMethod[] $breedingMethods
* @param Business[] $businesses
* @param ProductFormat[] $formats
* @param array<string, array{unit: NutritionnalUnit, value: string}> $nutritionalValues
*/
public function __construct(
public ProductId $id,
public string $name,
public string $seoTitle,
public string $seoDescription,
public ProductCategory $category,
public Technology $technology,
public bool $isEgalim,
public bool $isPublished,
public array $breedingMethods,
public array $businesses,
public array $formats,
public string $description,
public string $detailsAndAdvantages,
public string $handlingAdvices,
public ?string $ingredients,
public string $conservationBeforeOpening,
public ?string $conservationAfterOpening,
public ?StorageFilePath $picture,
public string $pictureAlt,
public string $pictureTitle,
public ?StorageFilePath $pdf,
public array $nutritionalValues,
) {
Assert::notEmpty($this->name);
Assert::notEmpty($this->seoTitle);
Assert::notEmpty($this->seoDescription);
Assert::notEmpty($this->description);
Assert::notEmpty($this->detailsAndAdvantages);
Assert::notEmpty($this->handlingAdvices);
Assert::notEmpty($this->conservationBeforeOpening);
Assert::notEmpty($this->pictureAlt);
Assert::notEmpty($this->pictureTitle);
Assert::allIsAOf($this->breedingMethods, BreedingMethod::class);
Assert::allIsAOf($this->businesses, Business::class);
Assert::allIsAOf($this->formats, ProductFormat::class);
Assert::notEmpty($this->breedingMethods);
Assert::notEmpty($this->businesses);
Assert::notEmpty($this->formats);
when($this->ingredients !== null, fn () => Assert::notEmpty($this->ingredients));
when($this->conservationAfterOpening !== null, fn () => Assert::notEmpty($this->conservationAfterOpening));
foreach ($this->nutritionalValues as $value) {
Assert::isArray($value);
Assert::keyExists($value, 'unit');
Assert::keyExists($value, 'value');
Assert::isInstanceOf($value['unit'], NutritionnalUnit::class);
Assert::string($value['value']);
}
}
}

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\Create;
use App\Contracts\Action;
use App\Contracts\ActionInput;
use App\Models\Recipe;
use Domain\Models\ValueObjects\Entities\RecipeId;
use Ramsey\Uuid\Uuid;
use Webmozart\Assert\Assert;
/**
* phpcs:disable Generic.Files.LineLength.TooLong
*
* @implements \App\Contracts\Action<\App\Cqs\Actions\Recipes\Create\CreateRecipeInput, \Domain\Models\ValueObjects\Entities\RecipeId|\App\Cqs\Actions\Recipes\Create\FailedToCreateRecipe>
* phpcs:enable
*/
final class CreateRecipe implements Action
{
/**
* @param \App\Cqs\Actions\Recipes\Create\CreateRecipeInput $input
*/
public function __invoke(ActionInput $input): RecipeId | FailedToCreateRecipe
{
Assert::isInstanceOf($input, CreateRecipeInput::class);
$recipe = new Recipe([
'seo_title' => $input->seoTitle,
'seo_description' => $input->seoDescription,
'name' => $input->name,
'is_published' => $input->isPublished,
'picture' => $input->picture,
'picture_alt' => $input->pictureAlt,
'picture_title' => $input->pictureTitle,
'product_category' => $input->productCategory,
'consomption_time' => $input->consomptionTime,
'inspiration' => $input->inspiration,
'season' => $input->season,
'description' => $input->description,
'details' => $input->details,
'ingredients_list' => $input->ingredientsList,
'preparation_steps' => $input->preparationSteps,
'tips_and_advices' => $input->tipsAndAdvices,
]);
if (! $recipe->save()) {
return new FailedToCreateRecipe();
}
$recipe->products()->sync($input->products);
return new RecipeId(Uuid::fromString($recipe->id));
}
}

View file

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\Create;
use App\Contracts\ActionInput;
use Domain\Models\ValueObjects\Products\ProductCategory;
use Domain\Models\ValueObjects\Recipes\ConsumptionTime;
use Domain\Models\ValueObjects\Recipes\Inspiration;
use Domain\Models\ValueObjects\Recipes\Season;
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
use Webmozart\Assert\Assert;
final readonly class CreateRecipeInput implements ActionInput
{
/**
* @param string[] $products
*/
public function __construct(
public string $name,
public string $seoTitle,
public string $seoDescription,
public bool $isPublished,
public ProductCategory $productCategory,
public ConsumptionTime $consomptionTime,
public Inspiration $inspiration,
public Season $season,
public ?string $description,
public ?string $details,
public string $ingredientsList,
public string $preparationSteps,
public ?string $tipsAndAdvices,
public ?StorageFilePath $picture,
public string $pictureAlt,
public string $pictureTitle,
public array $products = [],
) {
Assert::notEmpty($this->name);
Assert::notEmpty($this->seoTitle);
Assert::notEmpty($this->seoDescription);
Assert::notEmpty($this->ingredientsList);
Assert::notEmpty($this->preparationSteps);
Assert::notEmpty($this->pictureAlt);
Assert::notEmpty($this->pictureTitle);
when($this->description !== null, fn () => Assert::notEmpty($this->description));
when($this->details !== null, fn () => Assert::notEmpty($this->details));
when($this->tipsAndAdvices !== null, fn () => Assert::notEmpty($this->tipsAndAdvices));
Assert::allStringNotEmpty($this->products);
}
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\Create;
final readonly class FailedToCreateRecipe
{
}

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\Delete;
use App\Contracts\Action;
use App\Contracts\ActionInput;
use App\Models\Recipe;
use Domain\Models\ValueObjects\Entities\RecipeId;
use Ramsey\Uuid\Uuid;
use Webmozart\Assert\Assert;
/**
* phpcs:disable Generic.Files.LineLength.TooLong
*
* @implements \App\Contracts\Action<\App\Cqs\Actions\Recipes\Delete\DeleteRecipeInput, \Domain\Models\ValueObjects\Entities\RecipeId|\App\Cqs\Actions\Recipes\Delete\FailedToDeleteRecipe>
* phpcs:enable
*/
final class DeleteRecipe implements Action
{
/**
* @param \App\Cqs\Actions\Recipes\Delete\DeleteRecipeInput $input
*/
public function __invoke(ActionInput $input): RecipeId | FailedToDeleteRecipe
{
Assert::isInstanceOf($input, DeleteRecipeInput::class);
$recipe = Recipe::query()->findOrFail((string) $input->id->value);
if (! $recipe->delete()) {
return new FailedToDeleteRecipe();
}
return new RecipeId(Uuid::fromString($recipe->id));
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\Delete;
use App\Contracts\ActionInput;
use Domain\Models\ValueObjects\Entities\RecipeId;
final readonly class DeleteRecipeInput implements ActionInput
{
public function __construct(
public RecipeId $id,
) {
}
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\Delete;
final readonly class FailedToDeleteRecipe
{
}

View file

@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\Duplicate;
use App\Contracts\Action;
use App\Contracts\ActionInput;
use App\Lib\PublicS3Disk;
use App\Models\Recipe;
use Domain\Models\ValueObjects\Entities\RecipeId;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Ramsey\Uuid\Uuid;
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
use Webmozart\Assert\Assert;
/**
* phpcs:disable Generic.Files.LineLength.TooLong
*
* @implements \App\Contracts\Action<\App\Cqs\Actions\Recipes\Duplicate\DuplicateRecipeInput, \Domain\Models\ValueObjects\Entities\RecipeId|\App\Cqs\Actions\Recipes\Duplicate\FailedToDuplicateRecipe>
* phpcs:enable
*/
final class DuplicateRecipe implements Action
{
public function __construct(
private readonly PublicS3Disk $s3Disk,
) {
}
/**
* @param \App\Cqs\Actions\Recipes\Duplicate\DuplicateRecipeInput $input
*/
public function __invoke(ActionInput $input): RecipeId | FailedToDuplicateRecipe
{
Assert::isInstanceOf($input, DuplicateRecipeInput::class);
$original = Recipe::withTrashed()->with('products')->findOrFail((string) $input->recipeId->value);
$clone = $original->replicate();
$clone->name = $this->generateUniqueName($original->name);
$clone->slug = Str::slug($clone->name);
$duplicatedPicture = $this->duplicateFile($original->picture);
if ($duplicatedPicture !== null) {
$clone->picture = $duplicatedPicture;
}
$clone->is_published = false;
if (! $clone->save()) {
return new FailedToDuplicateRecipe();
}
$productIds = $original->products->pluck('id')->all();
if (! empty($productIds)) {
$clone->products()->sync($productIds);
}
return new RecipeId(Uuid::fromString($clone->id));
}
private function generateUniqueName(string $originalName): string
{
$rootName = $originalName;
if (preg_match('/^(.*?)(?: \(Copie(?: (\d+))?\))?$/', $originalName, $matches)) {
$rootName = $matches[1];
}
$baseName = "{$rootName} (Copie)";
if (
! Recipe::withTrashed()
->where('name', $baseName)
->orWhere('slug', Str::slug($baseName))
->exists()
) {
return $baseName;
}
$i = 2;
while (
Recipe::withTrashed()
->where('name', "{$rootName} (Copie {$i})")
->orWhere('slug', Str::slug("{$rootName} (Copie {$i})"))->exists()
) {
$i++;
}
return "{$rootName} (Copie {$i})";
}
private function duplicateFile(?StorageFilePath $filePath): ?StorageFilePath
{
if ($filePath === null) {
return null;
}
$sourcePath = $filePath->value;
$disk = Storage::disk($this->s3Disk->disk->value);
if (! $disk->exists($sourcePath)) {
return null;
}
$extension = pathinfo($sourcePath, PATHINFO_EXTENSION);
$dirname = pathinfo($sourcePath, PATHINFO_DIRNAME);
$newFilename = Str::uuid()->toString() . ($extension !== '' ? '.' . $extension : '');
$destinationPath = ($dirname !== '.' && $dirname !== '') ? "{$dirname}/{$newFilename}" : $newFilename;
if (! $disk->copy($sourcePath, $destinationPath)) {
return null;
}
return new StorageFilePath($destinationPath);
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\Duplicate;
use App\Contracts\ActionInput;
use Domain\Models\ValueObjects\Entities\RecipeId;
final readonly class DuplicateRecipeInput implements ActionInput
{
public function __construct(
public RecipeId $recipeId,
) {
}
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\Duplicate;
final readonly class FailedToDuplicateRecipe
{
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\ForceDelete;
final readonly class FailedToForceDeleteRecipe
{
}

View file

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\ForceDelete;
use App\Contracts\Action;
use App\Contracts\ActionInput;
use App\Lib\PublicS3Disk;
use App\Models\Recipe;
use Domain\Models\ValueObjects\Entities\RecipeId;
use Ramsey\Uuid\Uuid;
use Uwil\Storage\Contracts\StorageService;
use Webmozart\Assert\Assert;
/**
* phpcs:disable Generic.Files.LineLength.TooLong
*
* @implements \App\Contracts\Action<\App\Cqs\Actions\Recipes\ForceDelete\ForceDeleteRecipeInput, \Domain\Models\ValueObjects\Entities\RecipeId|\App\Cqs\Actions\Recipes\ForceDelete\FailedToForceDeleteRecipe>
* phpcs:enable
*/
final class ForceDeleteRecipe implements Action
{
public function __construct(
private readonly StorageService $storageService,
private readonly PublicS3Disk $s3Disk,
) {
}
/**
* @param \App\Cqs\Actions\Recipes\ForceDelete\ForceDeleteRecipeInput $input
*/
public function __invoke(ActionInput $input): RecipeId | FailedToForceDeleteRecipe
{
Assert::isInstanceOf($input, ForceDeleteRecipeInput::class);
$recipe = Recipe::withTrashed()->findOrFail((string) $input->id->value);
$picture = $recipe->picture;
if (! $recipe->forceDelete()) {
return new FailedToForceDeleteRecipe();
}
if ($picture !== null) {
$this->storageService->removeFromDisk($picture, $this->s3Disk->disk);
}
return new RecipeId(Uuid::fromString($recipe->id));
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\ForceDelete;
use App\Contracts\ActionInput;
use Domain\Models\ValueObjects\Entities\RecipeId;
final readonly class ForceDeleteRecipeInput implements ActionInput
{
public function __construct(
public RecipeId $id,
) {
}
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\Restore;
final readonly class FailedToRestoreRecipe
{
}

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\Restore;
use App\Contracts\Action;
use App\Contracts\ActionInput;
use App\Models\Recipe;
use Domain\Models\ValueObjects\Entities\RecipeId;
use Ramsey\Uuid\Uuid;
use Webmozart\Assert\Assert;
/**
* phpcs:disable Generic.Files.LineLength.TooLong
*
* @implements \App\Contracts\Action<\App\Cqs\Actions\Recipes\Restore\RestoreRecipeInput, \Domain\Models\ValueObjects\Entities\RecipeId|\App\Cqs\Actions\Recipes\Restore\FailedToRestoreRecipe>
* phpcs:enable
*/
final class RestoreRecipe implements Action
{
/**
* @param \App\Cqs\Actions\Recipes\Restore\RestoreRecipeInput $input
*/
public function __invoke(ActionInput $input): RecipeId | FailedToRestoreRecipe
{
Assert::isInstanceOf($input, RestoreRecipeInput::class);
$recipe = Recipe::withTrashed()->findOrFail((string) $input->id->value);
if (! $recipe->restore()) {
return new FailedToRestoreRecipe();
}
return new RecipeId(Uuid::fromString($recipe->id));
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\Restore;
use App\Contracts\ActionInput;
use Domain\Models\ValueObjects\Entities\RecipeId;
final readonly class RestoreRecipeInput implements ActionInput
{
public function __construct(
public RecipeId $id,
) {
}
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\Update;
final readonly class FailedToUpdateRecipe
{
}

View file

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\Update;
use App\Contracts\Action;
use App\Contracts\ActionInput;
use App\Dto\RecipeDto;
use App\Lib\PublicS3Disk;
use App\Models\Recipe;
use Uwil\Storage\Contracts\StorageService;
use Webmozart\Assert\Assert;
/**
* phpcs:disable Generic.Files.LineLength.TooLong
*
* @implements \App\Contracts\Action<\App\Cqs\Actions\Recipes\Update\UpdateRecipeInput, \App\Dto\RecipeDto|\App\Cqs\Actions\Recipes\Update\FailedToUpdateRecipe>
* phpcs:enable
*/
final class UpdateRecipe implements Action
{
public function __construct(
private readonly StorageService $storageService,
private readonly PublicS3Disk $s3Disk,
) {
}
/**
* @param \App\Cqs\Actions\Recipes\Update\UpdateRecipeInput $input
*/
public function __invoke(ActionInput $input): RecipeDto | FailedToUpdateRecipe
{
Assert::isInstanceOf($input, UpdateRecipeInput::class);
$recipe = Recipe::query()->findOrFail((string) $input->id->value);
$oldPicture = $recipe->picture;
$recipe->fill([
'seo_title' => $input->seoTitle,
'seo_description' => $input->seoDescription,
'name' => $input->name,
'is_published' => $input->isPublished,
'picture_alt' => $input->pictureAlt,
'picture_title' => $input->pictureTitle,
'product_category' => $input->productCategory,
'consomption_time' => $input->consomptionTime,
'inspiration' => $input->inspiration,
'season' => $input->season,
'description' => $input->description,
'details' => $input->details,
'ingredients_list' => $input->ingredientsList,
'preparation_steps' => $input->preparationSteps,
'tips_and_advices' => $input->tipsAndAdvices,
]);
if ($input->picture !== null) {
$recipe->picture = $input->picture;
}
if (! $recipe->save()) {
return new FailedToUpdateRecipe();
}
$recipe->products()->sync($input->products);
// Clean up replaced S3 picture
if ($input->picture !== null && $oldPicture !== null && $oldPicture->value !== $input->picture->value) {
$this->storageService->removeFromDisk($oldPicture, $this->s3Disk->disk);
}
return RecipeDto::fromModel($recipe, $this->s3Disk);
}
}

View file

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Actions\Recipes\Update;
use App\Contracts\ActionInput;
use Domain\Models\ValueObjects\Entities\RecipeId;
use Domain\Models\ValueObjects\Products\ProductCategory;
use Domain\Models\ValueObjects\Recipes\ConsumptionTime;
use Domain\Models\ValueObjects\Recipes\Inspiration;
use Domain\Models\ValueObjects\Recipes\Season;
use Uwil\Storage\Lib\ValueObjects\StorageFilePath;
use Webmozart\Assert\Assert;
final readonly class UpdateRecipeInput implements ActionInput
{
/**
* @param string[] $products
*/
public function __construct(
public RecipeId $id,
public string $name,
public string $seoTitle,
public string $seoDescription,
public bool $isPublished,
public ProductCategory $productCategory,
public ConsumptionTime $consomptionTime,
public Inspiration $inspiration,
public Season $season,
public ?string $description,
public ?string $details,
public string $ingredientsList,
public string $preparationSteps,
public ?string $tipsAndAdvices,
public ?StorageFilePath $picture,
public string $pictureAlt,
public string $pictureTitle,
public array $products = [],
) {
Assert::notEmpty($this->name);
Assert::notEmpty($this->seoTitle);
Assert::notEmpty($this->seoDescription);
Assert::notEmpty($this->ingredientsList);
Assert::notEmpty($this->preparationSteps);
Assert::notEmpty($this->pictureAlt);
Assert::notEmpty($this->pictureTitle);
when($this->description !== null, fn () => Assert::notEmpty($this->description));
when($this->details !== null, fn () => Assert::notEmpty($this->details));
when($this->tipsAndAdvices !== null, fn () => Assert::notEmpty($this->tipsAndAdvices));
Assert::allStringNotEmpty($this->products);
}
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Products;
use App\Contracts\QueryInput;
use Ramsey\Uuid\UuidInterface;
final readonly class FindProductByIdInput implements QueryInput
{
public function __construct(
public UuidInterface $id,
public bool $includeTrashed = false
) {
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Products;
use App\Contracts\QueryInput;
final readonly class FindProductBySlugInput implements QueryInput
{
public function __construct(
public string $slug,
public bool $includeTrashed = false
) {
}
}

View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Products;
use App\Contracts\Query;
use App\Contracts\QueryInput;
use App\Dto\ProductDto;
use App\Lib\PublicS3Disk;
use App\Models\Product;
use Webmozart\Assert\Assert;
/**
* @phpcs:disable Generic.Files.LineLength
*
* @implements \App\Contracts\Query<\App\Cqs\Queries\Products\FindProductByIdInput|\App\Cqs\Queries\Products\FindProductBySlugInput, \App\Dto\ProductDto|null>
*
* @phpcs:enable
*/
final readonly class FindProductQuery implements Query
{
public function __construct(
private PublicS3Disk $publicS3Disk,
) {
}
#[\Override]
public function __invoke(QueryInput $input): ?ProductDto
{
Assert::true(
$input instanceof FindProductByIdInput || $input instanceof FindProductBySlugInput,
'Input must be an instance of FindProductByIdInput or FindProductBySlugInput.'
);
$query = Product::query();
if ($input->includeTrashed) {
$query->withTrashed();
}
/** @var Product|null $product */
$product = match (true) {
$input instanceof FindProductByIdInput => $query->where('id', (string) $input->id)->first(),
$input instanceof FindProductBySlugInput => $query->where('slug', $input->slug)->first(),
};
if ($product === null) {
return null;
}
return ProductDto::fromModel($product, $this->publicS3Disk);
}
}

View file

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Products;
use App\Contracts\Query;
use App\Contracts\QueryInput;
use App\Models\Product;
use Domain\Models\ValueObjects\Products\ProductCategory;
use Domain\Models\ValueObjects\Products\Technology;
use Illuminate\Database\Eloquent\Builder;
use SortDirection;
use Webmozart\Assert\Assert;
/**
* phpcs:disable Generic.Files.LineLength
*
* @implements \App\Contracts\Query<\App\Cqs\Queries\Products\GetBoPaginatedProductsQueryInput, \App\Cqs\Queries\Products\GetBoPaginatedProductsQueryOutput>
* phpcs:enable
*/
final readonly class GetBoPaginatedProductsQuery implements Query
{
private const array SORTABLE_COLUMNS = [
'name',
'category',
'technology',
'is_egalim',
'is_published',
'created_at',
'updated_at',
];
#[\Override]
public function __invoke(QueryInput $input): GetBoPaginatedProductsQueryOutput
{
Assert::isAOf($input, GetBoPaginatedProductsQueryInput::class);
/** @var Builder<Product> $query */
$query = Product::query();
if ($input->isTrash) {
$query->onlyTrashed();
}
if ($input->search !== null && $input->search !== '') {
$words = array_filter(explode(' ', $input->search));
$query->where(function (Builder $q) use ($words, $input): void {
foreach ($words as $word) {
$q->where('name', 'like', "%{$word}%");
}
$q->orWhere('name', 'like', "%{$input->search}%");
});
}
$sortCol = in_array($input->sortCol, self::SORTABLE_COLUMNS, true)
? $input->sortCol
: 'name';
$direction = $input->sortDirection === SortDirection::Descending ? 'desc' : 'asc';
$query->orderBy($sortCol, $direction);
$paginated = $query->paginate(
perPage: $input->pagination->pageSize(),
page: $input->pagination->pageNumber(),
)->withQueryString();
$searchFields = \implode(', ', [
trans('validation.attributes.name'),
]);
return new GetBoPaginatedProductsQueryOutput(
paginatedProducts: $paginated,
search: $input->search,
searchFields: $searchFields,
categories: ProductCategory::toArray(),
technologies: Technology::toArray(),
);
}
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Products;
use App\Contracts\QueryInput;
use Domain\Models\ValueObjects\Pagination;
use SortDirection;
final readonly class GetBoPaginatedProductsQueryInput implements QueryInput
{
public function __construct(
public Pagination $pagination,
public ?string $search = null,
public string $sortCol = 'name',
public SortDirection $sortDirection = SortDirection::Ascending,
public bool $isTrash = false,
) {
}
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Products;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
final readonly class GetBoPaginatedProductsQueryOutput
{
/**
* @param LengthAwarePaginator<int,\App\Models\Product> $paginatedProducts
* @param array<string, mixed> $categories
* @param array<string, mixed> $technologies
*/
public function __construct(
public LengthAwarePaginator $paginatedProducts,
public ?string $search,
public string $searchFields,
public array $categories,
public array $technologies,
) {
}
}

View file

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Products;
use App\Contracts\Directories\ProductDirectory;
use App\Contracts\Query;
use App\Contracts\QueryInput;
use Domain\Models\ValueObjects\Products\ProductCategory;
use Webmozart\Assert\Assert;
/**
* @implements \App\Contracts\Query<\App\Cqs\Queries\Products\GetCategoriesProductsQueryInput, array<int, \App\Dto\CategoryProductsDto>>
*/
final readonly class GetCategoriesProductsQuery implements Query
{
public function __construct(
private ProductDirectory $productDirectory,
) {
}
/**
* @return array<int, \App\Dto\CategoryProductsDto>
*/
public function __invoke(QueryInput $input): array
{
Assert::isAOf($input, GetCategoriesProductsQueryInput::class);
$categories = $input->categories;
if (count($categories) === 0) {
$categories = ! $input->hasFilter ? [
ProductCategory::OMELETS_AND_DELI_AND_DESSERT_LINES,
ProductCategory::HARD_BOILED_EGGS,
] : ProductCategory::cases();
}
$categoriesProducts = [];
foreach ($categories as $category) {
$result = $this->productDirectory->getCategoryProducts(
category: $category,
breedingMethods: $input->breedingMethods,
technologies: $input->technologies,
businesses: $input->businesses,
isEgalim: $input->isEgalim,
search: $input->search,
limitPerCategory: -1
);
if ($result !== null) {
$categoriesProducts[] = $result;
}
}
return $categoriesProducts;
}
}

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Products;
use App\Contracts\QueryInput;
use Domain\Models\ValueObjects\Products\BreedingMethod;
use Domain\Models\ValueObjects\Products\Business;
use Domain\Models\ValueObjects\Products\ProductCategory;
use Domain\Models\ValueObjects\Products\Technology;
use Webmozart\Assert\Assert;
final readonly class GetCategoriesProductsQueryInput implements QueryInput
{
/**
* @param ProductCategory[] $categories
* @param BreedingMethod[] $breedingMethods
* @param Technology[] $technologies
* @param Business[] $businesses
*/
public function __construct(
public array $categories = [],
public array $breedingMethods = [],
public array $technologies = [],
public array $businesses = [],
public ?bool $isEgalim = null,
public string $search = '',
public bool $hasFilter = false,
) {
Assert::allIsAOf($this->categories, ProductCategory::class);
Assert::allIsAOf($this->breedingMethods, BreedingMethod::class);
Assert::allIsAOf($this->technologies, Technology::class);
Assert::allIsAOf($this->businesses, Business::class);
}
}

View file

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Products;
use App\Contracts\Directories\ProductDirectory;
use App\Contracts\Query;
use App\Contracts\QueryInput;
use App\Contracts\QueryInputEmpty;
use App\Dto\ProductSummaryDto;
use App\Lib\PublicS3Disk;
use Domain\Models\ValueObjects\Products\ProductSummary;
use Illuminate\Support\Collection;
use Webmozart\Assert\Assert;
/**
* @implements \App\Contracts\Query<\App\Contracts\QueryInputEmpty, \Illuminate\Support\Collection<int, \App\Dto\ProductSummaryDto>>
*/
final readonly class GetProductsSummariesQuery implements Query
{
public function __construct(
private ProductDirectory $productDirectory,
private PublicS3Disk $publicS3Disk
) {
}
/**
* @return \Illuminate\Support\Collection<int, \App\Dto\ProductSummaryDto>
*/
public function __invoke(QueryInput $input): Collection
{
Assert::isAOf(
$input,
QueryInputEmpty::class,
);
return Collection::make($this->productDirectory->getAllProductsSummaries())
->map(fn (ProductSummary $product): ProductSummaryDto => $this->mapToDto($product));
}
private function mapToDto(ProductSummary $product): ProductSummaryDto
{
return new ProductSummaryDto(
id: $product->id,
name: $product->name,
pictureUrl: $product->picture ?
$this->publicS3Disk->publicUrlOf($product->picture) : null,
);
}
}

View file

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Products;
use App\Contracts\Directories\ProductDirectory;
use App\Contracts\Query;
use App\Contracts\QueryInput;
use App\Dto\ProductDto;
use App\Lib\PublicS3Disk;
use Domain\Models\Entities\Product as DomainProduct;
use Webmozart\Assert\Assert;
/**
* @implements \App\Contracts\Query<\App\Cqs\Queries\Products\GetRelatedProductsQueryInput, array<\App\Dto\ProductDto>>
*/
final readonly class GetRelatedProductsQuery implements Query
{
public function __construct(
private ProductDirectory $productDirectory,
private PublicS3Disk $publicS3Disk,
) {
}
/**
* @return array< ProductDto>
*/
public function __invoke(QueryInput $input): array
{
Assert::isAOf($input, GetRelatedProductsQueryInput::class);
$products = $this->productDirectory->getRelatedProducts($input->recipeId, $input->limit);
return array_map(
fn (DomainProduct $product): ProductDto => ProductDto::fromEntity(
$product,
$this->publicS3Disk
),
$products
);
}
}

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Products;
use App\Contracts\QueryInput;
use Domain\Models\ValueObjects\Entities\RecipeId;
use Webmozart\Assert\Assert;
final readonly class GetRelatedProductsQueryInput implements QueryInput
{
public function __construct(
public RecipeId $recipeId,
public int $limit = 10,
) {
Assert::greaterThan($this->limit, 0);
}
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Recipes;
use App\Contracts\QueryInput;
use Ramsey\Uuid\UuidInterface;
final readonly class FindRecipeByIdInput implements QueryInput
{
public function __construct(
public UuidInterface $id,
public bool $includeTrashed = false
) {
}
}

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Recipes;
use App\Contracts\QueryInput;
final readonly class FindRecipeBySlugInput implements QueryInput
{
public function __construct(
public string $slug,
public bool $includeTrashed = false
) {
}
}

View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Recipes;
use App\Contracts\Query;
use App\Contracts\QueryInput;
use App\Dto\RecipeDto;
use App\Lib\PublicS3Disk;
use App\Models\Recipe;
use Webmozart\Assert\Assert;
/**
* @phpcs:disable Generic.Files.LineLength
*
* @implements \App\Contracts\Query<\App\Cqs\Queries\Recipes\FindRecipeByIdInput|\App\Cqs\Queries\Recipes\FindRecipeBySlugInput, \App\Dto\RecipeDto|null>
*
* @phpcs:enable
*/
final readonly class FindRecipeQuery implements Query
{
public function __construct(
private PublicS3Disk $publicS3Disk,
) {
}
#[\Override]
public function __invoke(QueryInput $input): ?RecipeDto
{
Assert::true(
$input instanceof FindRecipeByIdInput || $input instanceof FindRecipeBySlugInput,
'Input must be an instance of FindRecipeByIdInput or FindRecipeBySlugInput.'
);
$query = Recipe::query();
if ($input->includeTrashed) {
$query->withTrashed();
}
/** @var Recipe|null $recipe */
$recipe = match (true) {
$input instanceof FindRecipeByIdInput => $query->where('id', (string) $input->id)->first(),
$input instanceof FindRecipeBySlugInput => $query->where('slug', $input->slug)->first(),
};
if ($recipe === null) {
return null;
}
return RecipeDto::fromModel($recipe, $this->publicS3Disk);
}
}

View file

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Recipes;
use App\Contracts\Query;
use App\Contracts\QueryInput;
use App\Models\Recipe;
use Domain\Models\ValueObjects\Products\ProductCategory;
use Domain\Models\ValueObjects\Recipes\ConsumptionTime;
use Domain\Models\ValueObjects\Recipes\Inspiration;
use Domain\Models\ValueObjects\Recipes\Season;
use Illuminate\Database\Eloquent\Builder;
use SortDirection;
use Webmozart\Assert\Assert;
/**
* phpcs:disable Generic.Files.LineLength
*
* @implements \App\Contracts\Query<\App\Cqs\Queries\Recipes\GetBoPaginatedRecipesQueryInput, \App\Cqs\Queries\Recipes\GetBoPaginatedRecipesQueryOutput>
* phpcs:enable
*/
final readonly class GetBoPaginatedRecipesQuery implements Query
{
private const array SORTABLE_COLUMNS = [
'name',
'product_category',
'consomption_time',
'inspiration',
'season',
'is_published',
'created_at',
'updated_at',
];
#[\Override]
public function __invoke(QueryInput $input): GetBoPaginatedRecipesQueryOutput
{
Assert::isAOf($input, GetBoPaginatedRecipesQueryInput::class);
/** @var Builder<Recipe> $query */
$query = Recipe::query();
if ($input->isTrash) {
$query->onlyTrashed();
}
if ($input->search !== null && $input->search !== '') {
$words = array_filter(explode(' ', $input->search));
$query->where(function (Builder $q) use ($words, $input): void {
foreach ($words as $word) {
$q->where('name', 'like', "%{$word}%");
}
$q->orWhere('name', 'like', "%{$input->search}%");
});
}
$sortCol = in_array($input->sortCol, self::SORTABLE_COLUMNS, true)
? $input->sortCol
: 'name';
$direction = $input->sortDirection === SortDirection::Descending ? 'desc' : 'asc';
$query->orderBy($sortCol, $direction);
$paginated = $query->paginate(
perPage: $input->pagination->pageSize(),
page: $input->pagination->pageNumber(),
)->withQueryString();
$searchFields = \implode(', ', [
trans('validation.attributes.name'),
]);
return new GetBoPaginatedRecipesQueryOutput(
paginatedRecipes: $paginated,
search: $input->search,
searchFields: $searchFields,
categories: ProductCategory::toArray(),
consumptionTimes: ConsumptionTime::toArray(),
inspirations: Inspiration::toArray(),
seasons: Season::toArray(),
);
}
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Recipes;
use App\Contracts\QueryInput;
use Domain\Models\ValueObjects\Pagination;
use SortDirection;
final readonly class GetBoPaginatedRecipesQueryInput implements QueryInput
{
public function __construct(
public Pagination $pagination,
public ?string $search = null,
public string $sortCol = 'name',
public SortDirection $sortDirection = SortDirection::Ascending,
public bool $isTrash = false,
) {
}
}

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Recipes;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
final readonly class GetBoPaginatedRecipesQueryOutput
{
/**
* @param LengthAwarePaginator<int,\App\Models\Recipe> $paginatedRecipes
* @param array<string, mixed> $categories
* @param array<string, mixed> $consumptionTimes
* @param array<string, mixed> $inspirations
* @param array<string, mixed> $seasons
*/
public function __construct(
public LengthAwarePaginator $paginatedRecipes,
public ?string $search,
public string $searchFields,
public array $categories,
public array $consumptionTimes,
public array $inspirations,
public array $seasons,
) {
}
}

View file

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Recipes;
use App\Contracts\Directories\RecipeDirectory;
use App\Contracts\Query;
use App\Contracts\QueryInput;
use App\Dto\RecipeDto;
use App\Lib\PublicS3Disk;
use Domain\Models\ValueObjects\Pagination;
use Illuminate\Pagination\LengthAwarePaginator;
use Webmozart\Assert\Assert;
/**
* phpcs:disable Generic.Files.LineLength.TooLong
*
* @implements \App\Contracts\Query<\App\Cqs\Queries\Recipes\GetFrontRecipesQueryInput, \Illuminate\Pagination\LengthAwarePaginator<array-key, \App\Dto\RecipeDto>>
*
* phpcs:enable
*/
final readonly class GetFrontRecipesQuery implements Query
{
private const int PER_PAGE = 100;
public function __construct(
private RecipeDirectory $recipeDirectory,
private PublicS3Disk $publicS3Disk,
) {
}
/**
* @return LengthAwarePaginator<array-key, \App\Dto\RecipeDto>
*/
public function __invoke(QueryInput $input): LengthAwarePaginator
{
Assert::isAOf($input, GetFrontRecipesQueryInput::class);
// * Load all recipes for client-side display
$allRecipes = [];
$page = 1;
do {
$pagination = Pagination::ofPage($page, self::PER_PAGE);
$paginatedRecipes = $this->recipeDirectory->getPaginatedRecipes(
pagination: $pagination,
productRanges: $input->categories,
consumptionTimes: $input->consumptionTimes,
inspirations: $input->inspirations,
seasons: $input->seasons,
search: $input->search
);
foreach ($paginatedRecipes->items() as $recipe) {
$allRecipes[] = RecipeDto::fromEntity($recipe, $this->publicS3Disk);
}
$page++;
} while ($page <= $paginatedRecipes->lastPage());
return new LengthAwarePaginator(
items: $allRecipes,
total: count($allRecipes),
perPage: max(1, count($allRecipes)),
currentPage: 1,
);
}
}

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Recipes;
use App\Contracts\QueryInput;
use Domain\Models\ValueObjects\Products\ProductCategory;
use Domain\Models\ValueObjects\Recipes\ConsumptionTime;
use Domain\Models\ValueObjects\Recipes\Inspiration;
use Domain\Models\ValueObjects\Recipes\Season;
use Webmozart\Assert\Assert;
final readonly class GetFrontRecipesQueryInput implements QueryInput
{
/**
* @param ProductCategory[] $categories
* @param ConsumptionTime[] $consumptionTimes
* @param Inspiration[] $inspirations
* @param Season[] $seasons
*/
public function __construct(
public array $categories = [],
public array $consumptionTimes = [],
public array $inspirations = [],
public array $seasons = [],
public string $search = '',
) {
Assert::allIsAOf($this->categories, ProductCategory::class);
Assert::allIsAOf($this->consumptionTimes, ConsumptionTime::class);
Assert::allIsAOf($this->inspirations, Inspiration::class);
Assert::allIsAOf($this->seasons, Season::class);
}
}

View file

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Recipes;
use App\Contracts\Directories\RecipeDirectory;
use App\Contracts\Query;
use App\Contracts\QueryInput;
use App\Dto\RecipeDto;
use App\Lib\PublicS3Disk;
use Domain\Models\Entities\Recipe as DomainRecipe;
use Webmozart\Assert\Assert;
/**
* @implements \App\Contracts\Query<\App\Cqs\Queries\Recipes\GetRelatedRecipesQueryInput, array<\App\Dto\RecipeDto>>
*/
final readonly class GetRelatedRecipesQuery implements Query
{
public function __construct(
private RecipeDirectory $recipeDirectory,
private PublicS3Disk $publicS3Disk,
) {
}
/**
* @return array<RecipeDto>
*/
public function __invoke(QueryInput $input): array
{
Assert::isAOf($input, GetRelatedRecipesQueryInput::class);
$recipes = $this->recipeDirectory->getRelatedRecipes($input->productId, $input->limit);
return array_map(
fn (DomainRecipe $recipe): RecipeDto => RecipeDto::fromEntity(
$recipe,
$this->publicS3Disk
),
$recipes
);
}
}

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Cqs\Queries\Recipes;
use App\Contracts\QueryInput;
use Domain\Models\ValueObjects\Entities\ProductId;
use Webmozart\Assert\Assert;
final readonly class GetRelatedRecipesQueryInput implements QueryInput
{
public function __construct(
public ProductId $productId,
public int $limit = 10,
) {
Assert::greaterThan($this->limit, 0);
}
}

View file

@ -0,0 +1,228 @@
<?php
declare(strict_types=1);
namespace App\Directories;
use App\Contracts\Directories\ProductDirectory;
use App\Dto\CategoryProductsDto;
use App\Models\Product;
use App\Models\Recipe;
use Domain\Models\Entities\Product as DomainProduct;
use Domain\Models\ValueObjects\Entities\ProductId;
use Domain\Models\ValueObjects\Entities\RecipeId;
use Domain\Models\ValueObjects\Pagination;
use Domain\Models\ValueObjects\Products\ProductCategory;
use Domain\Models\ValueObjects\Products\ProductSummary;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
class EloquentProductDirectory implements ProductDirectory
{
public function find(ProductId $id): ?DomainProduct
{
return Product::query()
->with(['recipes'])
->where('id', (string) $id->value)
->limit(1)
->get()
->map(fn (Product $product) => $this->mapToEntity($product))
->first();
}
public function findBySlug(string $slug): ?DomainProduct
{
return Product::query()
->with(['recipes'])
->where('slug', $slug)
->limit(1)
->get()
->map(fn (Product $product) => $this->mapToEntity($product))
->first();
}
public function getAllProductsSummaries(): array
{
return Product::query()
->select(['id', 'name', 'picture'])
->orderBy('name', 'asc')
->get()
->map(fn (Product $product) => $this->mapToSummary($product))
->all();
}
public function getPaginatedProducts(
Pagination $pagination,
string $search = '',
): LengthAwarePaginator {
$dbQueryCallback = function (Builder $query) {
/** @var \Illuminate\Database\Eloquent\Builder<Product> $query */
$query->with(['recipes']);
};
if (! empty($search)) {
$scout = Product::search($search)->query($dbQueryCallback);
/** @var \Illuminate\Pagination\LengthAwarePaginator<array-key,Product> $paginated */
$paginated = $scout->paginate(
$pagination->pageSize(),
'page',
$pagination->pageNumber()
);
return $paginated->through(fn ($product) => $this->mapToEntity($product));
}
/** @var \Illuminate\Database\Eloquent\Builder<Product> $query */
$query = Product::query();
$dbQueryCallback($query);
return $query->paginate(
perPage: $pagination->pageSize(),
page: $pagination->pageNumber()
)->through(fn (Product $product) => $this->mapToEntity($product));
}
/**
* @param \Domain\Models\ValueObjects\Products\BreedingMethod[] $breedingMethods
* @param \Domain\Models\ValueObjects\Products\Technology[] $technologies
* @param \Domain\Models\ValueObjects\Products\Business[] $businesses
*/
public function getCategoryProducts(
ProductCategory $category,
array $breedingMethods = [],
array $technologies = [],
array $businesses = [],
?bool $isEgalim = null,
string $search = '',
int $limitPerCategory = 6
): ?CategoryProductsDto {
$dbQueryCallback = function (Builder $query) use ($category, $breedingMethods, $technologies, $businesses, $isEgalim) {
/** @var \Illuminate\Database\Eloquent\Builder<Product> $query */
$query->where('category', $category->value)
->where('is_published', true)
->with(['recipes']);
if (! empty($breedingMethods)) {
$query->where(function ($q) use ($breedingMethods) {
foreach ($breedingMethods as $method) {
$q->orWhereJsonContains('breeding_methods', $method->value);
}
});
}
if (! empty($technologies)) {
$techValues = array_map(fn ($t) => $t->value, $technologies);
$query->whereIn('technology', $techValues);
}
if (! empty($businesses)) {
$query->where(function ($q) use ($businesses) {
foreach ($businesses as $business) {
$q->orWhereJsonContains('businesses', $business->value);
}
});
}
if ($isEgalim === true) {
$query->where('is_egalim', true);
}
};
$query = Product::query();
if (! empty($search)) {
$scout = Product::search($search);
// Get all matching IDs from Meilisearch
$matchingIds = $scout->keys();
if ($matchingIds->isEmpty()) {
return null;
}
$query->whereIn('id', $matchingIds);
}
$dbQueryCallback($query);
$count = $query->count();
if ($count === 0) {
return null;
}
$products = (clone $query)->limit($limitPerCategory)
->get()
->map(fn (Product $product) => $this->mapToEntity($product))
->all();
return new CategoryProductsDto(
category: $category,
total: $count,
products: $products,
);
}
private function mapToSummary(Product $product): ProductSummary
{
return new ProductSummary(
id: (string) $product->id,
name: $product->name,
picture: $product->picture?->value,
);
}
private function mapToEntity(Product $product): DomainProduct
{
return new DomainProduct(
id: new ProductId(Uuid::fromString($product->id)),
name: $product->name,
slug: $product->slug,
category: $product->category,
technology: $product->technology,
isEgalim: $product->is_egalim,
isPublished: (bool) $product->is_published,
breedingMethods: $product->breeding_methods->all(),
businesses: $product->businesses->all(),
detailsAndAdvantages: $product->details_and_advantages,
formats: $product->formats->all(),
handlingAdvices: $product->handling_advices,
ingredients: $product->ingredients,
conservationBeforeOpening: $product->conservation_before_opening,
description: $product->description,
seoTitle: $product->seo_title,
seoDescription: $product->seo_description,
conservationAfterOpening: $product->conservation_after_opening,
nutritionalValues: $product->nutritional_values,
picture: $product->picture?->value,
pictureAlt: $product->picture_alt,
pictureTitle: $product->picture_title,
pdfFilePath: $product->pdf?->value,
associatedRecipes: $product->relationLoaded('recipes')
? $product->recipes->map(
fn (Recipe $recipe): UuidInterface => $recipe->getId()->value
)->all()
: [],
);
}
public function getRelatedProducts(RecipeId $recipeId, int $limit = 4): array
{
$recipe = Recipe::query()
->where('id', $recipeId->value)
->first();
if (! $recipe) {
return [];
}
return $recipe->products()
->where('is_published', true)
->limit($limit)
->get()
->map(fn (Product $product) => $this->mapToEntity($product))
->all();
}
}

View file

@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace App\Directories;
use App\Contracts\Directories\RecipeDirectory;
use App\Models\Product;
use App\Models\Recipe;
use Domain\Models\Entities\Recipe as DomainRecipe;
use Domain\Models\ValueObjects\Entities\ProductId;
use Domain\Models\ValueObjects\Entities\RecipeId;
use Domain\Models\ValueObjects\Pagination;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
use Ramsey\Uuid\Uuid;
class EloquentRecipeDirectory implements RecipeDirectory
{
public function find(RecipeId $id): ?DomainRecipe
{
return Recipe::query()
->with(['products'])
->where('id', (string) $id->value)
->limit(1)
->get()
->map(fn (Recipe $recipe) => $this->mapToEntity($recipe))
->first();
}
public function findBySlug(string $slug): ?DomainRecipe
{
return Recipe::query()
->with(['products'])
->where('slug', $slug)
->limit(1)
->get()
->map(fn (Recipe $recipe) => $this->mapToEntity($recipe))
->first();
}
public function getPaginatedRecipes(
Pagination $pagination,
array $productRanges = [],
array $consumptionTimes = [],
array $inspirations = [],
array $seasons = [],
string $search = ''
): LengthAwarePaginator {
$dbQueryCallback = function (Builder $query) use ($productRanges, $consumptionTimes, $inspirations, $seasons) {
/** @var \Illuminate\Database\Eloquent\Builder<Recipe> $query */
$query->with(['products'])
->where('is_published', true);
if (! empty($productRanges)) {
$rangesValues = array_map(fn ($r) => $r->value, $productRanges);
$query->whereIn('product_category', $rangesValues);
}
if (! empty($consumptionTimes)) {
$timesValues = array_map(fn ($t) => $t->value, $consumptionTimes);
$query->whereIn('consomption_time', $timesValues);
}
if (! empty($inspirations)) {
$inspValues = array_map(fn ($i) => $i->value, $inspirations);
$query->whereIn('inspiration', $inspValues);
}
if (! empty($seasons)) {
$seasonValues = array_map(fn ($s) => $s->value, $seasons);
$query->whereIn('season', $seasonValues);
}
};
$query = Recipe::query();
if (! empty($search)) {
$scout = Recipe::search($search);
$matchingIds = $scout->keys();
if ($matchingIds->isEmpty()) {
/** @var \Illuminate\Pagination\LengthAwarePaginator<array-key,DomainRecipe> */
return $query->whereIn('id', [])->paginate(
perPage: $pagination->pageSize(),
page: $pagination->pageNumber()
);
}
$query->whereIn('id', $matchingIds);
}
$dbQueryCallback($query);
return $query->paginate(
perPage: $pagination->pageSize(),
page: $pagination->pageNumber()
)->through(fn (Recipe $recipe) => $this->mapToEntity($recipe));
}
private function mapToEntity(Recipe $recipe): DomainRecipe
{
return new DomainRecipe(
id: new RecipeId(Uuid::fromString($recipe->id)),
name: $recipe->name,
slug: $recipe->slug,
isPublished: (bool) $recipe->is_published,
picture: $recipe->picture?->value,
seoTitle: $recipe->seo_title,
seoDescription: $recipe->seo_description,
productRange: $recipe->product_category,
consomptionTime: $recipe->consomption_time,
inspiration: $recipe->inspiration,
season: $recipe->season,
description: $recipe->description,
details: $recipe->details,
preparationTime: '15 min de préparation',
guestsCount: 4,
ingredientsList: $recipe->ingredients_list,
preparationSteps: $recipe->preparation_steps,
tipsAndAdvices: $recipe->tips_and_advices,
associatedProducts: []
);
}
public function getRelatedRecipes(ProductId $productId, int $limit = 4): array
{
$product = Product::query()
->where('id', (string) $productId->value)
->first();
if (! $product) {
return [];
}
return $product->recipes()
->where('is_published', true)
->limit($limit)
->get()
->map(fn (Recipe $recipe) => $this->mapToEntity($recipe))
->all();
}
}

View file

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Dto;
use App\Lib\PublicS3Disk;
use Domain\Models\Entities\Product;
use Domain\Models\ValueObjects\Products\ProductCategory;
use Illuminate\Support\Arr;
use Override;
final readonly class CategoryProductsDto implements \JsonSerializable
{
public function __construct(
public ProductCategory $category,
public int $total,
/** @var \Domain\Models\Entities\Product[] */
public array $products,
) {
}
#[Override]
public function jsonSerialize(): mixed
{
return [
'category' => [
'name' => $this->category->name(),
'label' => $this->category->label(),
'value' => $this->category->value(),
],
'total' => $this->total,
'products' => array_map(
[$this, 'productsToArray'],
$this->products
),
];
}
/** @return array<array-key,mixed> */
private function productsToArray(Product $product): array
{
$publicS3Disk = app(PublicS3Disk::class);
$data = Arr::wrap(json_decode(json_encode($product) ?: '', true));
if (isset($data['picture']) && is_string($data['picture'])) {
$data['picture'] = $publicS3Disk->publicUrlOf($data['picture']);
}
if (isset($data['pdfFilePath']) && is_string($data['pdfFilePath'])) {
$data['pdfFilePath'] = $publicS3Disk->publicUrlOf($data['pdfFilePath']);
}
return array_merge(
$data,
[
'id' => (string) $product->id->value,
]
);
}
}

242
app/Dto/ProductDto.php Normal file
View file

@ -0,0 +1,242 @@
<?php
declare(strict_types=1);
namespace App\Dto;
use App\Lib\PublicS3Disk;
use App\Models\Product;
use Carbon\CarbonInterface;
use Domain\Models\Entities\Product as DomainProduct;
use Domain\Models\ValueObjects\Entities\ProductId;
use Domain\Models\ValueObjects\Products\NutritionalValues;
use Domain\Models\ValueObjects\Products\ProductCategory;
use Domain\Models\ValueObjects\Products\Technology;
use Illuminate\Contracts\Routing\UrlRoutable;
use Illuminate\Contracts\Support\Arrayable;
use Ramsey\Uuid\Uuid;
/**
* @implements Arrayable<string, mixed>
*/
final readonly class ProductDto implements \JsonSerializable, Arrayable, UrlRoutable
{
/**
* @param \Domain\Models\ValueObjects\Products\BreedingMethod[] $breedingMethods
* @param \Domain\Models\ValueObjects\Products\Business[] $businesses
* @param \Domain\Models\ValueObjects\Products\ProductFormat[] $formats
*/
public function __construct(
public string $id,
public string $seoTitle,
public string $seoDescription,
public string $name,
public ?string $picture,
public ?string $pictureUrl,
public string $pictureAlt,
public string $pictureTitle,
public string $slug,
public ProductCategory $category,
public Technology $technology,
public bool $isEgalim,
public bool $isPublished,
public array $breedingMethods,
public array $businesses,
public array $formats,
public string $description,
public string $detailsAndAdvantages,
public string $handlingAdvices,
public ?string $ingredients,
public string $conservationBeforeOpening,
public ?string $conservationAfterOpening,
public ?NutritionalValues $nutritionalValues,
public ?string $pdf,
public ?string $pdfUrl,
public ?CarbonInterface $createdAt,
public ?CarbonInterface $updatedAt,
public ?CarbonInterface $deletedAt,
) {
}
public static function fromModel(Product $product, PublicS3Disk $publicS3Disk): self
{
$picturePath = $product->picture?->value;
$pdfPath = $product->pdf?->value;
return new self(
id: (string) $product->id,
seoTitle: (string) $product->seo_title,
seoDescription: (string) $product->seo_description,
name: (string) $product->name,
picture: $picturePath,
pictureUrl: $picturePath !== null ? $publicS3Disk->publicUrlOf($picturePath) : null,
pictureAlt: (string) $product->picture_alt,
pictureTitle: (string) $product->picture_title,
slug: (string) $product->slug,
category: $product->category,
technology: $product->technology,
isEgalim: (bool) $product->is_egalim,
isPublished: (bool) $product->is_published,
breedingMethods: $product->breeding_methods->values()->all(),
businesses: $product->businesses->values()->all(),
formats: $product->formats->values()->all(),
description: (string) $product->description,
detailsAndAdvantages: (string) $product->details_and_advantages,
handlingAdvices: (string) $product->handling_advices,
ingredients: $product->ingredients,
conservationBeforeOpening: (string) $product->conservation_before_opening,
conservationAfterOpening: $product->conservation_after_opening,
nutritionalValues: $product->nutritional_values,
pdf: $pdfPath,
pdfUrl: $pdfPath !== null ? $publicS3Disk->publicUrlOf($pdfPath) : null,
createdAt: $product->created_at,
updatedAt: $product->updated_at,
deletedAt: $product->deleted_at,
);
}
public static function fromEntity(
DomainProduct $product,
PublicS3Disk $publicS3Disk
): self {
$picturePath = $product->picture;
$pdfPath = $product->pdfFilePath;
return new self(
id: (string) $product->id->value,
seoTitle: (string) ($product->seoTitle ?? ''),
seoDescription: (string) ($product->seoDescription ?? ''),
name: $product->name,
picture: $picturePath,
pictureUrl: $picturePath !== null ? $publicS3Disk->publicUrlOf($picturePath) : null,
pictureAlt: (string) ($product->pictureAlt ?? ''),
pictureTitle: (string) ($product->pictureTitle ?? ''),
slug: $product->slug,
category: $product->category,
technology: $product->technology,
isEgalim: $product->isEgalim,
isPublished: $product->isPublished,
breedingMethods: $product->breedingMethods,
businesses: $product->businesses,
formats: $product->formats,
description: $product->description,
detailsAndAdvantages: $product->detailsAndAdvantages,
handlingAdvices: $product->handlingAdvices,
ingredients: $product->ingredients,
conservationBeforeOpening: $product->conservationBeforeOpening,
conservationAfterOpening: $product->conservationAfterOpening,
nutritionalValues: $product->nutritionalValues,
pdf: $pdfPath,
pdfUrl: $pdfPath !== null ? $publicS3Disk->publicUrlOf($pdfPath) : null,
createdAt: null,
updatedAt: null,
deletedAt: null,
);
}
public static function dummyForZiggy(): self
{
return new self(
id: 'dummy-id',
seoTitle: 'dummy-seo-title',
seoDescription: 'dummy-seo-description',
name: 'dummy-name',
picture: null,
pictureUrl: null,
pictureAlt: 'dummy-picture-alt',
pictureTitle: 'dummy-picture-title',
slug: 'dummy-slug',
category: ProductCategory::OMELETS_AND_DELI_AND_DESSERT_LINES,
technology: Technology::FRESH,
isEgalim: false,
isPublished: true,
breedingMethods: [],
businesses: [],
formats: [],
description: 'dummy-description',
detailsAndAdvantages: 'dummy-details-and-advantages',
handlingAdvices: 'dummy-handling-advices',
ingredients: 'dummy-ingredients',
conservationBeforeOpening: 'dummy-conservation-before-opening',
conservationAfterOpening: 'dummy-conservation-after-opening',
nutritionalValues: null,
pdf: null,
pdfUrl: null,
createdAt: null,
updatedAt: null,
deletedAt: null,
);
}
public function getId(): ProductId
{
return new ProductId(Uuid::fromString($this->id));
}
#[\Override]
public function getRouteKey(): mixed
{
return $this->id;
}
#[\Override]
public function getRouteKeyName(): string
{
return 'id';
}
#[\Override]
public function resolveRouteBinding($value, $field = null): mixed
{
return null;
}
#[\Override]
public function resolveChildRouteBinding($childType, $value, $field): mixed
{
return null;
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'seo_title' => $this->seoTitle,
'seo_description' => $this->seoDescription,
'picture' => $this->pictureUrl,
'picture_alt' => $this->pictureAlt,
'picture_title' => $this->pictureTitle,
'category' => $this->category->value,
'technology' => $this->technology->value,
'is_egalim' => $this->isEgalim,
'is_published' => $this->isPublished,
'breeding_methods' => $this->breedingMethods,
'businesses' => $this->businesses,
'formats' => $this->formats,
'description' => $this->description,
'details_and_advantages' => $this->detailsAndAdvantages,
'handling_advices' => $this->handlingAdvices,
'ingredients' => $this->ingredients,
'conservation_before_opening' => $this->conservationBeforeOpening,
'conservation_after_opening' => $this->conservationAfterOpening,
'nutritional_values' => $this->nutritionalValues?->toArray(),
'pdf' => $this->pdfUrl,
'created_at' => $this->createdAt?->toISOString(),
'updated_at' => $this->updatedAt?->toISOString(),
'deleted_at' => $this->deletedAt?->toISOString(),
];
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
}

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Dto;
final readonly class ProductSummaryDto
{
public function __construct(
public string $id,
public string $name,
public ?string $pictureUrl,
) {
}
}

208
app/Dto/RecipeDto.php Normal file
View file

@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
namespace App\Dto;
use App\Lib\PublicS3Disk;
use App\Models\Recipe;
use Carbon\CarbonInterface;
use Domain\Models\Entities\Recipe as DomainRecipe;
use Domain\Models\ValueObjects\Entities\RecipeId;
use Domain\Models\ValueObjects\Products\ProductCategory;
use Domain\Models\ValueObjects\Recipes\ConsumptionTime;
use Domain\Models\ValueObjects\Recipes\Inspiration;
use Domain\Models\ValueObjects\Recipes\Season;
use Illuminate\Contracts\Routing\UrlRoutable;
use Illuminate\Contracts\Support\Arrayable;
use JsonSerializable;
use Ramsey\Uuid\Uuid;
/**
* @implements Arrayable<string, mixed>
*/
final readonly class RecipeDto implements Arrayable, JsonSerializable, UrlRoutable
{
public function __construct(
public string $id,
public string $seoTitle,
public string $seoDescription,
public string $name,
public ?string $picture,
public ?string $pictureUrl,
public string $pictureAlt,
public string $pictureTitle,
public string $slug,
public bool $isPublished,
public ProductCategory $productCategory,
public ConsumptionTime $consomptionTime,
public Inspiration $inspiration,
public Season $season,
public ?string $description,
public ?string $details,
public string $ingredientsList,
public string $preparationSteps,
public ?string $tipsAndAdvices,
public ?CarbonInterface $createdAt,
public ?CarbonInterface $updatedAt,
public ?CarbonInterface $deletedAt,
) {
}
public static function fromModel(Recipe $recipe, PublicS3Disk $publicS3Disk): self
{
$picturePath = $recipe->picture?->value;
return new self(
id: (string) $recipe->id,
seoTitle: (string) $recipe->seo_title,
seoDescription: (string) $recipe->seo_description,
name: (string) $recipe->name,
picture: $picturePath,
pictureUrl: $picturePath !== null ? $publicS3Disk->publicUrlOf($picturePath) : null,
pictureAlt: (string) $recipe->picture_alt,
pictureTitle: (string) $recipe->picture_title,
slug: (string) $recipe->slug,
isPublished: (bool) $recipe->is_published,
productCategory: $recipe->product_category,
consomptionTime: $recipe->consomption_time,
inspiration: $recipe->inspiration,
season: $recipe->season,
description: $recipe->description,
details: $recipe->details,
ingredientsList: (string) $recipe->ingredients_list,
preparationSteps: (string) $recipe->preparation_steps,
tipsAndAdvices: $recipe->tips_and_advices,
createdAt: $recipe->created_at,
updatedAt: $recipe->updated_at,
deletedAt: $recipe->deleted_at,
);
}
public static function fromEntity(
DomainRecipe $recipe,
PublicS3Disk $publicS3Disk
): self {
$picturePath = $recipe->picture;
return new self(
id: (string) $recipe->id->value,
seoTitle: (string) ($recipe->seoTitle ?? ''),
seoDescription: (string) ($recipe->seoDescription ?? ''),
name: $recipe->name,
picture: $picturePath,
pictureUrl: $picturePath !== null ? $publicS3Disk->publicUrlOf($picturePath) : null,
pictureAlt: '',
pictureTitle: '',
slug: $recipe->slug,
isPublished: $recipe->isPublished,
productCategory: $recipe->productRange,
consomptionTime: $recipe->consomptionTime,
inspiration: $recipe->inspiration,
season: $recipe->season,
description: $recipe->description,
details: $recipe->details,
ingredientsList: $recipe->ingredientsList,
preparationSteps: $recipe->preparationSteps,
tipsAndAdvices: $recipe->tipsAndAdvices,
createdAt: null,
updatedAt: null,
deletedAt: null,
);
}
public static function dummyForZiggy(): self
{
return new self(
id: 'dummy-id',
seoTitle: 'dummy-seo-title',
seoDescription: 'dummy-seo-description',
name: 'dummy-name',
picture: null,
pictureUrl: null,
pictureAlt: 'dummy-picture-alt',
pictureTitle: 'dummy-picture-title',
slug: 'dummy-slug',
isPublished: true,
productCategory: ProductCategory::OMELETS_AND_DELI_AND_DESSERT_LINES,
consomptionTime: ConsumptionTime::LUNCH_DINNER,
inspiration: Inspiration::FRENCH_COOKING,
season: Season::ALL_YEAR,
description: 'dummy-description',
details: 'dummy-details',
ingredientsList: 'dummy-ingredients-list',
preparationSteps: 'dummy-preparation-steps',
tipsAndAdvices: 'dummy-tips-and-advices',
createdAt: null,
updatedAt: null,
deletedAt: null,
);
}
public function getId(): RecipeId
{
return new RecipeId(Uuid::fromString($this->id));
}
#[\Override]
public function getRouteKey(): mixed
{
return $this->id;
}
#[\Override]
public function getRouteKeyName(): string
{
return 'id';
}
#[\Override]
public function resolveRouteBinding($value, $field = null): mixed
{
return null;
}
#[\Override]
public function resolveChildRouteBinding($childType, $value, $field): mixed
{
return null;
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'is_published' => $this->isPublished,
'seo_title' => $this->seoTitle,
'seo_description' => $this->seoDescription,
'picture' => $this->pictureUrl,
'picture_alt' => $this->pictureAlt,
'picture_title' => $this->pictureTitle,
'product_category' => $this->productCategory->value,
'consomption_time' => $this->consomptionTime->value,
'inspiration' => $this->inspiration->value,
'season' => $this->season->value,
'description' => $this->description,
'details' => $this->details,
'ingredients_list' => $this->ingredientsList,
'preparation_steps' => $this->preparationSteps,
'tips_and_advices' => $this->tipsAndAdvices,
'created_at' => $this->createdAt?->toISOString(),
'updated_at' => $this->updatedAt?->toISOString(),
'deleted_at' => $this->deletedAt?->toISOString(),
];
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
}

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Enums\Contact;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ActivitySectorEnum: int
{
use BaseEnum;
case commercial_catering = 1;
case collective_catering = 2;
case bakery_pastry = 3;
case food_industry = 4;
case supermarkets = 5;
case others = 6;
/**
* Optionnal labels definition.
*
* @phpstan-ignore classConstant.unused
*/
private const LABELS = [
self::commercial_catering->name => 'Restauration Commerciale',
self::collective_catering->name => 'Restauration Collective',
self::bakery_pastry->name => 'Boulangerie-Pâtisserie',
self::food_industry->name => 'Industrie Agro-Alimentaire',
self::supermarkets->name => 'Grandes et Moyennes Surfaces',
self::others->name => 'Autres',
];
}

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Enums\Contact;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ObjectEnum: int
{
use BaseEnum;
case product_questions = 1;
case quote_request = 2;
case order_tracking = 3;
case partnership_request = 4;
case product_complaint = 5;
case other = 6;
/**
* Optionnal labels definition.
*
* @phpstan-ignore classConstant.unused
*/
private const LABELS = [
self::product_questions->name => 'Questions sur nos produits',
self::quote_request->name => 'Demande de devis',
self::order_tracking->name => 'Suivi de commande',
self::partnership_request->name => 'Demande de partenariat',
self::product_complaint->name => 'Réclamation produit',
self::other->name => 'Autre',
];
}

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Exceptions;
class AuthServiceException extends \RuntimeException
{
public static function of(?string $message = null, ?\Throwable $cause = null): self
{
return new self($message ?? 'An authentication error occurred.', previous: $cause);
}
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Exceptions\Helpers;
class FileStorageOperationException extends \RuntimeException
{
public static function copyFailed(string $sourcePath, string $destinationPath): self
{
return new self(sprintf('Failed to copy file %s to %s', $sourcePath, $destinationPath));
}
public static function cannotGenerateUniqueFilename(string $folderPath, string $filename): self
{
return new self("Failed to have a unique filename for {$folderPath}/{$filename}");
}
}

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Exceptions\Helpers;
class UnsupportedStructureConversionException extends \RuntimeException
{
public static function fromType(mixed $input): self
{
return new self('Cannot handle $input of type ' . \gettype($input));
}
}

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Exceptions;
class ImageOptimizerException extends \RuntimeException
{
public static function of(?string $message = null, ?\Throwable $cause = null): self
{
return new self($message ?? 'An image optimization error occurred.', previous: $cause);
}
}

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