refonte
This commit is contained in:
32
.dockerignore
Normal file
32
.dockerignore
Normal file
@@ -0,0 +1,32 @@
|
||||
**/*.log
|
||||
**/*.md
|
||||
**/*.php~
|
||||
**/*.dist.php
|
||||
**/*.dist
|
||||
**/*.cache
|
||||
**/._*
|
||||
**/.dockerignore
|
||||
**/.DS_Store
|
||||
**/.git/
|
||||
**/.gitattributes
|
||||
**/.gitignore
|
||||
**/.gitmodules
|
||||
**/compose.*.yaml
|
||||
**/compose.*.yml
|
||||
**/compose.yaml
|
||||
**/compose.yml
|
||||
**/docker-compose.*.yaml
|
||||
**/docker-compose.*.yml
|
||||
**/docker-compose.yaml
|
||||
**/docker-compose.yml
|
||||
**/Dockerfile
|
||||
**/Thumbs.db
|
||||
public/bundles/
|
||||
tests/
|
||||
var/
|
||||
vendor/
|
||||
.editorconfig
|
||||
.env.*.local
|
||||
.env.local
|
||||
.env.local.php
|
||||
.env.test
|
||||
58
.editorconfig
Normal file
58
.editorconfig
Normal file
@@ -0,0 +1,58 @@
|
||||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
# Change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# We recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,html,ts,tsx}]
|
||||
indent_size = 2
|
||||
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.sh]
|
||||
indent_style = tab
|
||||
|
||||
[*.xml{,.dist}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.{yaml,yml}]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[.github/workflows/*.yml]
|
||||
indent_size = 2
|
||||
|
||||
[.gitmodules]
|
||||
indent_style = tab
|
||||
|
||||
[.php_cs{,.dist}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[composer.json]
|
||||
indent_size = 4
|
||||
|
||||
[{,docker-}compose{,.*}.{yaml,yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[{,*.*}Dockerfile]
|
||||
indent_style = tab
|
||||
|
||||
[{,*.*}Caddyfile]
|
||||
indent_style = tab
|
||||
29
.env
Normal file
29
.env
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=dev
|
||||
APP_SECRET=b55256ade5b349fd83c703f098b219fe
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
POSTGRES_DB=app
|
||||
POSTGRES_USER=user
|
||||
POSTGRES_PASSWORD=password
|
||||
POSTGRES_VERSION=16
|
||||
POSTGRES_HOST=database
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
||||
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||
#
|
||||
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
|
||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
||||
# DATABASE_URL="postgresql://user:password@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
|
||||
|
||||
#DATABASE_URL="postgresql://%env(resolve:POSTGRES_USER)%:%env(resolve:POSTGRES_PASSWORD)%@%env(resolve:POSTGRES_HOST)%/%env(resolve:POSTGRES_DB)%?serverVersion=%env(resolve:POSTGRES_VERSION)%&charset=utf8"
|
||||
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
###> nelmio/cors-bundle ###
|
||||
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||
###< nelmio/cors-bundle ###
|
||||
24
.env.example
Normal file
24
.env.example
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=dev
|
||||
APP_SECRET=b55256ade5b349fd83c703f098b219fe
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
POSTGRES_DB=app
|
||||
POSTGRES_USER=user
|
||||
POSTGRES_PASSWORD=password
|
||||
POSTGRES_VERSION=16
|
||||
#POSTGRES_HOST=database
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
||||
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||
#
|
||||
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
|
||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
||||
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
||||
# DATABASE_URL="postgresql://user:password@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
|
||||
|
||||
DATABASE_URL="postgresql://%env(resolve:POSTGRES_USER)%:%env(resolve:POSTGRES_PASSWORD)%@%env(resolve:POSTGRES_HOST)%/%env(resolve:POSTGRES_DB)%?serverVersion=%env(resolve:POSTGRES_VERSION)%&charset=utf8"
|
||||
|
||||
###< doctrine/doctrine-bundle ###
|
||||
9
.env.test
Normal file
9
.env.test
Normal file
@@ -0,0 +1,9 @@
|
||||
# define your env variables for the test env here
|
||||
KERNEL_CLASS='App\Kernel'
|
||||
APP_SECRET='$ecretf0rt3st'
|
||||
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||
PANTHER_APP_ENV=panther
|
||||
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
|
||||
|
||||
POSTGRES_DB=app
|
||||
POSTGRE_VERSION=16
|
||||
17
.gitattributes
vendored
Normal file
17
.gitattributes
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.conf text eol=lf
|
||||
*.html text eol=lf
|
||||
*.ini text eol=lf
|
||||
*.js text eol=lf
|
||||
*.json text eol=lf
|
||||
*.md text eol=lf
|
||||
*.php text eol=lf
|
||||
*.sh text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.yml text eol=lf
|
||||
bin/console text eol=lf
|
||||
composer.lock text eol=lf merge=ours
|
||||
|
||||
*.ico binary
|
||||
*.png binary
|
||||
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
/.env.local
|
||||
/.env.local.php
|
||||
/.env.*.local
|
||||
/config/secrets/prod/prod.decrypt.private.php
|
||||
/public/bundles/
|
||||
/var/*
|
||||
/vendor/
|
||||
.idea/
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> friendsofphp/php-cs-fixer ###
|
||||
/.php-cs-fixer.php
|
||||
/.php-cs-fixer.cache
|
||||
###< friendsofphp/php-cs-fixer ###
|
||||
|
||||
###> symfony/phpunit-bridge ###
|
||||
.phpunit.result.cache
|
||||
/phpunit.xml
|
||||
###< symfony/phpunit-bridge ###
|
||||
|
||||
###> phpunit/phpunit ###
|
||||
/phpunit.xml
|
||||
.phpunit.result.cache
|
||||
###< phpunit/phpunit ###
|
||||
13
.php-cs-fixer.dist.php
Normal file
13
.php-cs-fixer.dist.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
$finder = (new PhpCsFixer\Finder())
|
||||
->in(__DIR__)
|
||||
->exclude('var')
|
||||
;
|
||||
|
||||
return (new PhpCsFixer\Config())
|
||||
->setRules([
|
||||
'@Symfony' => true,
|
||||
])
|
||||
->setFinder($finder)
|
||||
;
|
||||
94
Dockerfile
Normal file
94
Dockerfile
Normal file
@@ -0,0 +1,94 @@
|
||||
#syntax=docker/dockerfile:1.4
|
||||
|
||||
# Versions
|
||||
FROM dunglas/frankenphp:1-alpine AS frankenphp_upstream
|
||||
|
||||
# The different stages of this Dockerfile are meant to be built into separate images
|
||||
# https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage
|
||||
# https://docs.docker.com/compose/compose-file/#target
|
||||
|
||||
|
||||
# Base FrankenPHP image
|
||||
FROM frankenphp_upstream AS frankenphp_base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# persistent / runtime deps
|
||||
# hadolint ignore=DL3018
|
||||
RUN apk add --no-cache \
|
||||
acl \
|
||||
file \
|
||||
gettext \
|
||||
git \
|
||||
;
|
||||
|
||||
RUN set -eux; \
|
||||
install-php-extensions \
|
||||
@composer \
|
||||
apcu \
|
||||
intl \
|
||||
opcache \
|
||||
zip \
|
||||
;
|
||||
|
||||
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
|
||||
ENV COMPOSER_ALLOW_SUPERUSER=1
|
||||
|
||||
###> recipes ###
|
||||
###> doctrine/doctrine-bundle ###
|
||||
RUN install-php-extensions pdo_pgsql
|
||||
###< doctrine/doctrine-bundle ###
|
||||
###< recipes ###
|
||||
|
||||
COPY --link frankenphp/conf.d/app.ini $PHP_INI_DIR/conf.d/
|
||||
COPY --link --chmod=755 frankenphp/docker-entrypoint.sh /usr/local/bin/docker-entrypoint
|
||||
COPY --link frankenphp/Caddyfile /etc/caddy/Caddyfile
|
||||
|
||||
ENTRYPOINT ["docker-entrypoint"]
|
||||
|
||||
HEALTHCHECK --start-period=60s CMD curl -f http://localhost:2019/metrics || exit 1
|
||||
CMD [ "frankenphp", "run", "--config", "/etc/caddy/Caddyfile" ]
|
||||
|
||||
# Dev FrankenPHP image
|
||||
FROM frankenphp_base AS frankenphp_dev
|
||||
|
||||
ENV APP_ENV=dev XDEBUG_MODE=off
|
||||
VOLUME /app/var/
|
||||
|
||||
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
RUN set -eux; \
|
||||
install-php-extensions \
|
||||
xdebug \
|
||||
;
|
||||
|
||||
COPY --link frankenphp/conf.d/app.dev.ini $PHP_INI_DIR/conf.d/
|
||||
|
||||
CMD [ "frankenphp", "run", "--config", "/etc/caddy/Caddyfile", "--watch" ]
|
||||
|
||||
# Prod FrankenPHP image
|
||||
FROM frankenphp_base AS frankenphp_prod
|
||||
|
||||
ENV APP_ENV=prod
|
||||
ENV FRANKENPHP_CONFIG="import worker.Caddyfile"
|
||||
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
COPY --link frankenphp/conf.d/app.prod.ini $PHP_INI_DIR/conf.d/
|
||||
COPY --link frankenphp/worker.Caddyfile /etc/caddy/worker.Caddyfile
|
||||
|
||||
# prevent the reinstallation of vendors at every changes in the source code
|
||||
COPY --link composer.* symfony.* ./
|
||||
RUN set -eux; \
|
||||
composer install --no-cache --prefer-dist --no-dev --no-autoloader --no-scripts --no-progress
|
||||
|
||||
# copy sources
|
||||
COPY --link . ./
|
||||
RUN rm -Rf frankenphp/
|
||||
|
||||
RUN set -eux; \
|
||||
mkdir -p var/cache var/log; \
|
||||
composer dump-autoload --classmap-authoritative --no-dev; \
|
||||
composer dump-env prod; \
|
||||
composer run-script --no-dev post-install-cmd; \
|
||||
chmod +x bin/console; sync;
|
||||
125
Makefile
Normal file
125
Makefile
Normal file
@@ -0,0 +1,125 @@
|
||||
# Executables (local)
|
||||
DOCKER_COMP = docker compose
|
||||
|
||||
# Docker containers
|
||||
PHP_CONT = $(DOCKER_COMP) exec php
|
||||
|
||||
# Executables
|
||||
PHP = $(PHP_CONT) php
|
||||
COMPOSER = $(PHP_CONT) composer
|
||||
SYMFONY = $(PHP) bin/console
|
||||
SF_MEMORY = $(PHP) -d memory_limit=256M bin/console
|
||||
|
||||
# Misc
|
||||
.DEFAULT_GOAL = help
|
||||
.PHONY : help build start install down stop logs sh composer vendor sf cc test phpmd phpcs quality fix-permissions controller entity migration migration-diff migration-migrate form crud fixtures command auth subscriber state-processor state-provider
|
||||
|
||||
## —— 🎵 🐳 The Symfony Docker Makefile 🐳 🎵 ——————————————————————————————————
|
||||
help: ## Outputs this help screen
|
||||
@grep -E '(^[a-zA-Z0-9\./_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}{printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/'
|
||||
|
||||
## —— Docker 🐳 ————————————————————————————————————————————————————————————————
|
||||
build: ## Builds the Docker images
|
||||
@$(DOCKER_COMP) build --pull --no-cache
|
||||
|
||||
start: ## Start the docker hub in detached mode (no logs)
|
||||
@$(DOCKER_COMP) up --pull always -d --wait
|
||||
|
||||
install: build start vendor## Build and start the containers
|
||||
|
||||
down: ## Stop and remove the docker hub
|
||||
@$(DOCKER_COMP) down --remove-orphans
|
||||
|
||||
stop: ## Stop the docker hub
|
||||
@$(DOCKER_COMP) stop
|
||||
|
||||
logs: ## Show live logs
|
||||
@$(DOCKER_COMP) logs --tail=0 --follow
|
||||
|
||||
sh: ## Connect to the FrankenPHP container
|
||||
@$(PHP_CONT) sh
|
||||
|
||||
test: ## Start tests with phpunit, pass the parameter "c=" to add options to phpunit, example: make test c="--group e2e --stop-on-failure"
|
||||
@$(eval c ?=)
|
||||
@$(DOCKER_COMP) exec -e APP_ENV=test php bin/phpunit $(c)
|
||||
|
||||
phpmd: ## Start PHP Mess Detector
|
||||
@if ! $(DOCKER_COMP) exec php vendor/bin/phpmd src/ text phpmd.xml -v; then \
|
||||
echo "Done"; \
|
||||
fi
|
||||
|
||||
phpcs: ## Start PHP Code Sniffer
|
||||
@$(DOCKER_COMP) exec php vendor/bin/php-cs-fixer fix src/ --rules=@PSR12
|
||||
|
||||
quality: phpmd phpcs ## Start PHP Mess Detector and PHP Code Sniffer
|
||||
|
||||
fix-permissions: ## Fix permissions
|
||||
@$(DOCKER_COMP) run --rm --user="root" php chown -R $$(id -u):$$(id -g) .
|
||||
|
||||
tail-logs: ## Tail the logs
|
||||
@$(DOCKER_COMP) logs -f
|
||||
|
||||
## —— Composer 🧙 ——————————————————————————————————————————————————————————————
|
||||
composer: ## Run composer, pass the parameter "c=" to run a given command, example: make composer c='req symfony/orm-pack'
|
||||
@$(eval c ?=)
|
||||
@$(COMPOSER) $(c)
|
||||
|
||||
vendor: ## Install vendors according to the current composer.lock file
|
||||
vendor: c=install --prefer-dist --no-progress --no-scripts --no-interaction
|
||||
vendor: composer
|
||||
|
||||
## —— Symfony 🎵 ———————————————————————————————————————————————————————————————
|
||||
sf: ## List all Symfony commands or pass the parameter "c=" to run a given command, example: make sf c=about
|
||||
@$(eval c ?=)
|
||||
@$(SYMFONY) $(c)
|
||||
|
||||
cc: c=c:c ## Clear the cache
|
||||
cc: sf
|
||||
|
||||
controller: ## Create a new controller
|
||||
@$(SYMFONY) make:controller
|
||||
|
||||
entity: ## Create a new entity
|
||||
@$(SYMFONY) make:entity
|
||||
|
||||
migration: ## Create a new migration
|
||||
@$(SYMFONY) make:migration
|
||||
|
||||
migration-diff: ## Create a new migration diff
|
||||
@$(SYMFONY) make:migration --no-interaction --allow-empty-diff
|
||||
|
||||
migration-migrate: ## Migrate the database
|
||||
@$(SYMFONY) doctrine:migrations:migrate --no-interaction
|
||||
|
||||
migration-revert: ## Revert the last migration
|
||||
@$(SYMFONY) doctrine:migrations:migrate prev --no-interaction
|
||||
|
||||
form: ## Create a new form
|
||||
@$(SYMFONY) make:form
|
||||
|
||||
crud: ## Create CRUD for an existing entity
|
||||
@$(SYMFONY) make:crud
|
||||
|
||||
factory: ## Create a new factory
|
||||
@$(SYMFONY) make:factory
|
||||
|
||||
fixtures: ## Make fixtures
|
||||
@$(SYMFONY) make:fixtures
|
||||
|
||||
fixtures-load: ## Load fixtures
|
||||
@$(SF_MEMORY) doctrine:fixtures:load --no-interaction
|
||||
|
||||
command: ## Create a new command
|
||||
@$(SYMFONY) make:command
|
||||
|
||||
auth: ## Create a new authenticator
|
||||
@$(SYMFONY) make:auth
|
||||
|
||||
subscriber: ## Create a new event subscriber
|
||||
@$(SYMFONY) make:subscriber
|
||||
|
||||
state-processor: ## Create a new state processor
|
||||
@$(SYMFONY) make:state-processor
|
||||
|
||||
state-provider: ## Create a new state provider
|
||||
@$(SYMFONY) make:state-provider
|
||||
53
README.md
Normal file
53
README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Projet TKM Symfony
|
||||
|
||||
Ce projet est un fork du template Symfony disponible à [dunglas/symfony-docker](https://github.com/dunglas/symfony-docker), adapté aux besoins spécifiques de TKM. Il intègre Symfony 7.0.2 et est configuré pour fonctionner avec Docker, offrant une mise en place rapide et efficace pour le développement.
|
||||
|
||||
## Prérequis
|
||||
|
||||
Avant de commencer, assurez-vous que les outils suivants sont installés sur votre machine :
|
||||
- Docker
|
||||
- Makefile
|
||||
|
||||
## Installation
|
||||
|
||||
Pour mettre en place le projet, suivez ces étapes :
|
||||
|
||||
1. Clonez le dépôt du projet :
|
||||
```git clone git@bitbucket.org:tkm_rd/tkm-symfony.git```
|
||||
|
||||
2. Copiez le fichier `.env.example` en `.env` :
|
||||
```cp .env.example .env```
|
||||
|
||||
3. Modifiez les credentials dans le fichier `.env` selon vos besoins.
|
||||
|
||||
4. Lancez l'installation des dépendances et la configuration du projet :
|
||||
```make install```
|
||||
|
||||
Si vous rencontrez des problèmes de droits sur le projet, exécutez :
|
||||
```make fix-permissions```
|
||||
|
||||
## Utilisation
|
||||
|
||||
Pour démarrer le projet, utilisez :
|
||||
```make start```
|
||||
|
||||
Pour arrêter le projet, utilisez :
|
||||
```make stop```
|
||||
|
||||
Pour voir d'autres commandes utiles, vous pouvez lancer :
|
||||
```make help```
|
||||
|
||||
|
||||
## Composants
|
||||
|
||||
Le projet comprend les technologies et outils suivants :
|
||||
- **Symfony 7.0.2** : Framework PHP pour la construction d'applications web.
|
||||
- **PostgreSQL 16** : Système de gestion de base de données relationnelle.
|
||||
- **Caddy** : Serveur web moderne, sécurisé et facile à utiliser.
|
||||
- **FrankenPhp** : Environnement d'exécution pour applications PHP.
|
||||
- **phpmd (PHP Mess Detector)** : Outil d'analyse statique de code PHP.
|
||||
- **php-cs (PHP CodeSniffer)** : Outil pour détecter les violations de standards de codage PHP.
|
||||
|
||||
## Administration de la Base de Données
|
||||
|
||||
Admirer, équivalent de PhpMyAdmin, est disponible sur le port **8080** pour la gestion de la base de données. Les credentials sont spécifiés dans le fichier `.env`. Utilisez `database` comme nom de serveur, qui correspond au nom du conteneur Docker de la base de données.
|
||||
17
bin/console
Executable file
17
bin/console
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
|
||||
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
return function (array $context) {
|
||||
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
|
||||
return new Application($kernel);
|
||||
};
|
||||
23
bin/phpunit
Executable file
23
bin/phpunit
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
if (!ini_get('date.timezone')) {
|
||||
ini_set('date.timezone', 'UTC');
|
||||
}
|
||||
|
||||
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
|
||||
if (PHP_VERSION_ID >= 80000) {
|
||||
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
|
||||
} else {
|
||||
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
|
||||
require PHPUNIT_COMPOSER_INSTALL;
|
||||
PHPUnit\TextUI\Command::main();
|
||||
}
|
||||
} else {
|
||||
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
|
||||
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
|
||||
}
|
||||
30
compose.override.yaml
Normal file
30
compose.override.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
# Development environment override
|
||||
services:
|
||||
php:
|
||||
build:
|
||||
context: .
|
||||
target: frankenphp_dev
|
||||
volumes:
|
||||
- ./:/app
|
||||
- ./frankenphp/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- ./frankenphp/conf.d/app.dev.ini:/usr/local/etc/php/conf.d/app.dev.ini:ro
|
||||
# If you develop on Mac or Windows you can remove the vendor/ directory
|
||||
# from the bind-mount for better performance by enabling the next line:
|
||||
#- /app/vendor
|
||||
environment:
|
||||
MERCURE_EXTRA_DIRECTIVES: demo
|
||||
# See https://xdebug.org/docs/all_settings#mode
|
||||
XDEBUG_MODE: "${XDEBUG_MODE:-off}"
|
||||
extra_hosts:
|
||||
# Ensure that host.docker.internal is correctly defined on Linux
|
||||
- host.docker.internal:host-gateway
|
||||
tty: true
|
||||
|
||||
###> symfony/mercure-bundle ###
|
||||
###< symfony/mercure-bundle ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database:
|
||||
ports:
|
||||
- "5432"
|
||||
###< doctrine/doctrine-bundle ###
|
||||
10
compose.prod.yaml
Normal file
10
compose.prod.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
# Production environment override
|
||||
services:
|
||||
php:
|
||||
build:
|
||||
context: .
|
||||
target: frankenphp_prod
|
||||
environment:
|
||||
APP_SECRET: ${APP_SECRET}
|
||||
MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET}
|
||||
MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET}
|
||||
71
compose.yaml
Normal file
71
compose.yaml
Normal file
@@ -0,0 +1,71 @@
|
||||
services:
|
||||
php:
|
||||
image: ${IMAGES_PREFIX:-}app-php
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SERVER_NAME: ${SERVER_NAME:-localhost}, php:80
|
||||
MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
|
||||
MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
|
||||
TRUSTED_PROXIES: ${TRUSTED_PROXIES:-127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16}
|
||||
TRUSTED_HOSTS: ^${SERVER_NAME:-example\.com|localhost}|php$$
|
||||
# Run "composer require symfony/orm-pack" to install and configure Doctrine ORM
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-!ChangeMe!}@database:5432/${POSTGRES_DB:-app}?serverVersion=${POSTGRES_VERSION:-15}&charset=${POSTGRES_CHARSET:-utf8}
|
||||
# Run "composer require symfony/mercure-bundle" to install and configure the Mercure integration
|
||||
MERCURE_URL: ${CADDY_MERCURE_URL:-http://php/.well-known/mercure}
|
||||
MERCURE_PUBLIC_URL: https://${SERVER_NAME:-localhost}/.well-known/mercure
|
||||
MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
|
||||
# The two next lines can be removed after initial installation
|
||||
SYMFONY_VERSION: ${SYMFONY_VERSION:-}
|
||||
STABILITY: ${STABILITY:-stable}
|
||||
volumes:
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
ports:
|
||||
# HTTP
|
||||
- target: 80
|
||||
published: ${HTTP_PORT:-80}
|
||||
protocol: tcp
|
||||
# HTTPS
|
||||
- target: 443
|
||||
published: ${HTTPS_PORT:-443}
|
||||
protocol: tcp
|
||||
# HTTP/3
|
||||
- target: 443
|
||||
published: ${HTTP3_PORT:-443}
|
||||
protocol: udp
|
||||
|
||||
# Mercure is installed as a Caddy module, prevent the Flex recipe from installing another service
|
||||
###> symfony/mercure-bundle ###
|
||||
###< symfony/mercure-bundle ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database:
|
||||
image: postgres:${POSTGRES_VERSION:-16}-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-app}
|
||||
# You should definitely change the password in production
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-app}
|
||||
volumes:
|
||||
- database_data:/var/lib/postgresql/data:rw
|
||||
ports:
|
||||
- '5432:5432'
|
||||
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
|
||||
# - ./docker/db/data:/var/lib/postgresql/data:rw
|
||||
###< doctrine/doctrine-bundle ###
|
||||
adminer:
|
||||
image: adminer
|
||||
ports:
|
||||
- '8080:8080'
|
||||
depends_on:
|
||||
- database
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
###> symfony/mercure-bundle ###
|
||||
###< symfony/mercure-bundle ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
database_data:
|
||||
###< doctrine/doctrine-bundle ###
|
||||
104
composer.json
Normal file
104
composer.json
Normal file
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"name": "symfony/skeleton",
|
||||
"type": "project",
|
||||
"license": "MIT",
|
||||
"description": "A minimal Symfony project recommended to create bare bones applications",
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.3.1",
|
||||
"ext-ctype": "*",
|
||||
"ext-curl": "*",
|
||||
"ext-iconv": "*",
|
||||
"api-platform/core": "^3.2",
|
||||
"doctrine/doctrine-bundle": "^2.11",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.3",
|
||||
"doctrine/orm": "^2.17",
|
||||
"guzzlehttp/guzzle": "^7.8",
|
||||
"nelmio/cors-bundle": "^2.4",
|
||||
"phpdocumentor/reflection-docblock": "^5.3",
|
||||
"phpstan/phpdoc-parser": "^1.25",
|
||||
"runtime/frankenphp-symfony": "^0.2.0",
|
||||
"symfony/asset": "7.0.*",
|
||||
"symfony/console": "7.0.*",
|
||||
"symfony/dotenv": "7.0.*",
|
||||
"symfony/expression-language": "7.0.*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/framework-bundle": "7.0.*",
|
||||
"symfony/http-client": "7.0.*",
|
||||
"symfony/monolog-bundle": "^3.10",
|
||||
"symfony/property-access": "7.0.*",
|
||||
"symfony/property-info": "7.0.*",
|
||||
"symfony/runtime": "7.0.*",
|
||||
"symfony/security-bundle": "7.0.*",
|
||||
"symfony/serializer": "7.0.*",
|
||||
"symfony/twig-bundle": "7.0.*",
|
||||
"symfony/validator": "7.0.*",
|
||||
"symfony/yaml": "7.0.*"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true,
|
||||
"symfony/flex": true,
|
||||
"symfony/runtime": true
|
||||
},
|
||||
"sort-packages": true
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"App\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"replace": {
|
||||
"symfony/polyfill-ctype": "*",
|
||||
"symfony/polyfill-iconv": "*",
|
||||
"symfony/polyfill-php72": "*",
|
||||
"symfony/polyfill-php73": "*",
|
||||
"symfony/polyfill-php74": "*",
|
||||
"symfony/polyfill-php80": "*",
|
||||
"symfony/polyfill-php81": "*",
|
||||
"symfony/polyfill-php82": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"auto-scripts": {
|
||||
"cache:clear": "symfony-cmd",
|
||||
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||
},
|
||||
"post-install-cmd": [
|
||||
"@auto-scripts"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@auto-scripts"
|
||||
]
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
},
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "7.0.*",
|
||||
"docker": true
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.5",
|
||||
"friendsofphp/php-cs-fixer": "^3.48",
|
||||
"mtdowling/jmespath.php": "^2.7",
|
||||
"phpmd/phpmd": "^2.15",
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"symfony/browser-kit": "7.0.*",
|
||||
"symfony/css-selector": "7.0.*",
|
||||
"symfony/maker-bundle": "^1.52",
|
||||
"symfony/phpunit-bridge": "^7.0",
|
||||
"symfony/stopwatch": "7.0.*",
|
||||
"symfony/web-profiler-bundle": "7.0.*",
|
||||
"zenstruck/browser": "^1.8",
|
||||
"zenstruck/foundry": "^1.36"
|
||||
}
|
||||
}
|
||||
10359
composer.lock
generated
Normal file
10359
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
config/bundles.php
Normal file
16
config/bundles.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
Zenstruck\Foundry\ZenstruckFoundryBundle::class => ['dev' => true, 'test' => true],
|
||||
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||
];
|
||||
25
config/packages/api_platform.yaml
Normal file
25
config/packages/api_platform.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
api_platform:
|
||||
title: Mangarr API
|
||||
version: 1.0.0
|
||||
formats:
|
||||
jsonld: ['application/ld+json']
|
||||
json: ['application/json']
|
||||
html: ['text/html']
|
||||
jsonhal: ['application/hal+json']
|
||||
swagger:
|
||||
api_keys:
|
||||
access_token:
|
||||
name: Authorization
|
||||
type: header
|
||||
docs_formats:
|
||||
jsonld: ['application/ld+json']
|
||||
jsonopenapi: ['application/vnd.openapi+json']
|
||||
html: ['text/html']
|
||||
defaults:
|
||||
cache_headers:
|
||||
vary: ['Content-Type', 'Authorization', 'Origin']
|
||||
extra_properties:
|
||||
standard_put: true
|
||||
rfc_7807_compliant_errors: true
|
||||
event_listeners_backward_compatibility_layer: false
|
||||
keep_legacy_inflector: false
|
||||
19
config/packages/cache.yaml
Normal file
19
config/packages/cache.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
framework:
|
||||
cache:
|
||||
# Unique name of your app: used to compute stable namespaces for cache keys.
|
||||
#prefix_seed: your_vendor_name/app_name
|
||||
|
||||
# The "app" cache stores to the filesystem by default.
|
||||
# The data in this cache should persist between deploys.
|
||||
# Other options include:
|
||||
|
||||
# Redis
|
||||
#app: cache.adapter.redis
|
||||
#default_redis_provider: redis://localhost
|
||||
|
||||
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
|
||||
#app: cache.adapter.apcu
|
||||
|
||||
# Namespaced pools use the above "app" backend by default
|
||||
#pools:
|
||||
#my.dedicated.cache: null
|
||||
49
config/packages/doctrine.yaml
Normal file
49
config/packages/doctrine.yaml
Normal file
@@ -0,0 +1,49 @@
|
||||
doctrine:
|
||||
dbal:
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
|
||||
# IMPORTANT: You MUST configure your server version,
|
||||
# either here or in the DATABASE_URL env var (see .env file)
|
||||
#server_version: '16'
|
||||
|
||||
profiling_collect_backtrace: '%kernel.debug%'
|
||||
orm:
|
||||
auto_generate_proxy_classes: true
|
||||
enable_lazy_ghost_objects: true
|
||||
report_fields_where_declared: true
|
||||
validate_xml_mapping: true
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
|
||||
auto_mapping: true
|
||||
mappings:
|
||||
App:
|
||||
type: attribute
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: App
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
dbal:
|
||||
# "TEST_TOKEN" is typically set by ParaTest
|
||||
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
|
||||
|
||||
when@prod:
|
||||
doctrine:
|
||||
orm:
|
||||
auto_generate_proxy_classes: false
|
||||
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
|
||||
query_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.system_cache_pool
|
||||
result_cache_driver:
|
||||
type: pool
|
||||
pool: doctrine.result_cache_pool
|
||||
|
||||
framework:
|
||||
cache:
|
||||
pools:
|
||||
doctrine.result_cache_pool:
|
||||
adapter: cache.app
|
||||
doctrine.system_cache_pool:
|
||||
adapter: cache.system
|
||||
6
config/packages/doctrine_migrations.yaml
Normal file
6
config/packages/doctrine_migrations.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
doctrine_migrations:
|
||||
migrations_paths:
|
||||
# namespace is arbitrary but should be different from App\Migrations
|
||||
# as migrations classes should NOT be autoloaded
|
||||
'DoctrineMigrations': '%kernel.project_dir%/migrations'
|
||||
enable_profiler: false
|
||||
18
config/packages/framework.yaml
Normal file
18
config/packages/framework.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
#csrf_protection: true
|
||||
|
||||
# Note that the session will be started ONLY if you read or write from it.
|
||||
session:
|
||||
cookie_samesite: none
|
||||
cookie_secure: true
|
||||
|
||||
#esi: true
|
||||
#fragments: true
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
test: true
|
||||
session:
|
||||
storage_factory_id: session.storage.factory.mock_file
|
||||
62
config/packages/monolog.yaml
Normal file
62
config/packages/monolog.yaml
Normal file
@@ -0,0 +1,62 @@
|
||||
monolog:
|
||||
channels:
|
||||
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
|
||||
|
||||
when@dev:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event"]
|
||||
# uncomment to get logging in your browser
|
||||
# you may have to allow bigger header sizes in your Web server configuration
|
||||
#firephp:
|
||||
# type: firephp
|
||||
# level: info
|
||||
#chromephp:
|
||||
# type: chromephp
|
||||
# level: info
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!console"]
|
||||
|
||||
when@test:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
channels: ["!event"]
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
|
||||
when@prod:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||
nested:
|
||||
type: stream
|
||||
path: php://stderr
|
||||
level: debug
|
||||
formatter: monolog.formatter.json
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine"]
|
||||
deprecation:
|
||||
type: stream
|
||||
channels: [deprecation]
|
||||
path: php://stderr
|
||||
formatter: monolog.formatter.json
|
||||
11
config/packages/nelmio_cors.yaml
Normal file
11
config/packages/nelmio_cors.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
nelmio_cors:
|
||||
defaults:
|
||||
allow_credentials: true
|
||||
origin_regex: true
|
||||
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
|
||||
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
allow_headers: ['Content-Type', 'Authorization']
|
||||
expose_headers: ['Link', 'Location']
|
||||
max_age: 3600
|
||||
paths:
|
||||
'^/': ~
|
||||
10
config/packages/routing.yaml
Normal file
10
config/packages/routing.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
framework:
|
||||
router:
|
||||
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||
#default_uri: http://localhost
|
||||
|
||||
when@prod:
|
||||
framework:
|
||||
router:
|
||||
strict_requirements: null
|
||||
55
config/packages/security.yaml
Normal file
55
config/packages/security.yaml
Normal file
@@ -0,0 +1,55 @@
|
||||
security:
|
||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
providers:
|
||||
# used to reload user from session & other features (e.g. switch_user)
|
||||
app_user_provider:
|
||||
entity:
|
||||
class: App\Entity\User
|
||||
property: email
|
||||
|
||||
role_hierarchy:
|
||||
ROLE_FRONT_USER: [ROLE_USER_EDIT, ROLE_PROJECT_CREATE, ROLE_PROJECT_EDIT]
|
||||
|
||||
firewalls:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
main:
|
||||
lazy: true
|
||||
provider: app_user_provider
|
||||
json_login:
|
||||
check_path: app_login
|
||||
username_path: email
|
||||
password_path: password
|
||||
logout:
|
||||
path: app_logout
|
||||
access_token:
|
||||
token_handler: App\Security\ApiTokenHandler
|
||||
|
||||
# activate different ways to authenticate
|
||||
# https://symfony.com/doc/current/security.html#the-firewall
|
||||
|
||||
# https://symfony.com/doc/current/security/impersonating_user.html
|
||||
# switch_user: true
|
||||
|
||||
# Easy way to control access for large sections of your site
|
||||
# Note: Only the *first* access control that matches will be used
|
||||
access_control:
|
||||
# - { path: ^/admin, roles: ROLE_ADMIN }
|
||||
# - { path: ^/profile, roles: ROLE_USER }
|
||||
|
||||
when@test:
|
||||
security:
|
||||
password_hashers:
|
||||
# By default, password hashers are resource intensive and take time. This is
|
||||
# important to generate secure password hashes. In tests however, secure hashes
|
||||
# are not important, waste resources and increase test times. The following
|
||||
# reduces the work factor to the lowest possible values.
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||
algorithm: auto
|
||||
cost: 4 # Lowest possible value for bcrypt
|
||||
time_cost: 3 # Lowest possible value for argon
|
||||
memory_cost: 10 # Lowest possible value for argon
|
||||
6
config/packages/twig.yaml
Normal file
6
config/packages/twig.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
twig:
|
||||
file_name_pattern: '*.twig'
|
||||
|
||||
when@test:
|
||||
twig:
|
||||
strict_variables: true
|
||||
11
config/packages/validator.yaml
Normal file
11
config/packages/validator.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
framework:
|
||||
validation:
|
||||
# Enables validator auto-mapping support.
|
||||
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
|
||||
#auto_mapping:
|
||||
# App\Entity\: []
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
validation:
|
||||
not_compromised_password: false
|
||||
17
config/packages/web_profiler.yaml
Normal file
17
config/packages/web_profiler.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
when@dev:
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler:
|
||||
only_exceptions: false
|
||||
collect_serializer_data: true
|
||||
|
||||
when@test:
|
||||
web_profiler:
|
||||
toolbar: false
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { collect: false }
|
||||
7
config/packages/zenstruck_foundry.yaml
Normal file
7
config/packages/zenstruck_foundry.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
when@dev: &dev
|
||||
# See full configuration: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#full-default-bundle-configuration
|
||||
zenstruck_foundry:
|
||||
# Whether to auto-refresh proxies by default (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#auto-refresh)
|
||||
auto_refresh_proxies: true
|
||||
|
||||
when@test: *dev
|
||||
5
config/preload.php
Normal file
5
config/preload.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
|
||||
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
|
||||
}
|
||||
5
config/routes.yaml
Normal file
5
config/routes.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
controllers:
|
||||
resource:
|
||||
path: ../src/Controller/
|
||||
namespace: App\Controller
|
||||
type: attribute
|
||||
4
config/routes/api_platform.yaml
Normal file
4
config/routes/api_platform.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
api_platform:
|
||||
resource: .
|
||||
type: api_platform
|
||||
prefix: /api
|
||||
4
config/routes/framework.yaml
Normal file
4
config/routes/framework.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
when@dev:
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /_error
|
||||
3
config/routes/security.yaml
Normal file
3
config/routes/security.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
_security_logout:
|
||||
resource: security.route_loader.logout
|
||||
type: service
|
||||
8
config/routes/web_profiler.yaml
Normal file
8
config/routes/web_profiler.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
when@dev:
|
||||
web_profiler_wdt:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
|
||||
prefix: /_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
|
||||
prefix: /_profiler
|
||||
45
config/services.yaml
Normal file
45
config/services.yaml
Normal file
@@ -0,0 +1,45 @@
|
||||
# This file is the entry point to configure your own services.
|
||||
# Files in the packages/ subdirectory configure your dependencies.
|
||||
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
_defaults:
|
||||
autowire: true # Automatically injects dependencies in your services.
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/'
|
||||
exclude:
|
||||
- '../src/DependencyInjection/'
|
||||
- '../src/Entity/'
|
||||
- '../src/Kernel.php'
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
|
||||
App\EventListener\ExceptionListener:
|
||||
tags:
|
||||
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }
|
||||
|
||||
App\Service\DataLakeClient:
|
||||
arguments:
|
||||
$baseUrl: '%env(DATA_LAKE_BASE_URL)%'
|
||||
|
||||
GuzzleHttp\Client:
|
||||
class: GuzzleHttp\Client
|
||||
arguments:
|
||||
$config:
|
||||
headers:
|
||||
Content-Type: 'application/json'
|
||||
allow_redirects:
|
||||
max: 20
|
||||
strict: true
|
||||
referer: true
|
||||
protocols: [ 'http', 'https' ]
|
||||
track_redirects: true
|
||||
55
frankenphp/Caddyfile
Normal file
55
frankenphp/Caddyfile
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
{$CADDY_GLOBAL_OPTIONS}
|
||||
|
||||
frankenphp {
|
||||
{$FRANKENPHP_CONFIG}
|
||||
}
|
||||
|
||||
# https://caddyserver.com/docs/caddyfile/directives#sorting-algorithm
|
||||
order mercure after encode
|
||||
order vulcain after reverse_proxy
|
||||
order php_server before file_server
|
||||
}
|
||||
|
||||
{$CADDY_EXTRA_CONFIG}
|
||||
|
||||
{$SERVER_NAME:localhost} {
|
||||
log {
|
||||
# Redact the authorization query parameter that can be set by Mercure
|
||||
format filter {
|
||||
wrap console
|
||||
fields {
|
||||
uri query {
|
||||
replace authorization REDACTED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root * /app/public
|
||||
encode zstd gzip
|
||||
|
||||
mercure {
|
||||
# Transport to use (default to Bolt)
|
||||
transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
|
||||
# Publisher JWT key
|
||||
publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
|
||||
# Subscriber JWT key
|
||||
subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
|
||||
# Allow anonymous subscribers (double-check that it's what you want)
|
||||
anonymous
|
||||
# Enable the subscription API (double-check that it's what you want)
|
||||
subscriptions
|
||||
# Extra directives
|
||||
{$MERCURE_EXTRA_DIRECTIVES}
|
||||
}
|
||||
|
||||
vulcain
|
||||
|
||||
{$CADDY_SERVER_EXTRA_DIRECTIVES}
|
||||
|
||||
# Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
|
||||
header ?Permissions-Policy "browsing-topics=()"
|
||||
|
||||
php_server
|
||||
}
|
||||
5
frankenphp/conf.d/app.dev.ini
Normal file
5
frankenphp/conf.d/app.dev.ini
Normal file
@@ -0,0 +1,5 @@
|
||||
; See https://docs.docker.com/desktop/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host
|
||||
; See https://github.com/docker/for-linux/issues/264
|
||||
; The `client_host` below may optionally be replaced with `discover_client_host=yes`
|
||||
; Add `start_with_request=yes` to start debug session on each request
|
||||
xdebug.client_host = host.docker.internal
|
||||
14
frankenphp/conf.d/app.ini
Normal file
14
frankenphp/conf.d/app.ini
Normal file
@@ -0,0 +1,14 @@
|
||||
variables_order = EGPCS
|
||||
expose_php = 0
|
||||
date.timezone = UTC
|
||||
apc.enable_cli = 1
|
||||
session.use_strict_mode = 1
|
||||
zend.detect_unicode = 0
|
||||
|
||||
; https://symfony.com/doc/current/performance.html
|
||||
realpath_cache_size = 4096K
|
||||
realpath_cache_ttl = 600
|
||||
opcache.interned_strings_buffer = 16
|
||||
opcache.max_accelerated_files = 20000
|
||||
opcache.memory_consumption = 256
|
||||
opcache.enable_file_override = 1
|
||||
2
frankenphp/conf.d/app.prod.ini
Normal file
2
frankenphp/conf.d/app.prod.ini
Normal file
@@ -0,0 +1,2 @@
|
||||
opcache.preload_user = root
|
||||
opcache.preload = /app/config/preload.php
|
||||
60
frankenphp/docker-entrypoint.sh
Executable file
60
frankenphp/docker-entrypoint.sh
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then
|
||||
# Install the project the first time PHP is started
|
||||
# After the installation, the following block can be deleted
|
||||
if [ ! -f composer.json ]; then
|
||||
rm -Rf tmp/
|
||||
composer create-project "symfony/skeleton $SYMFONY_VERSION" tmp --stability="$STABILITY" --prefer-dist --no-progress --no-interaction --no-install
|
||||
|
||||
cd tmp
|
||||
cp -Rp . ..
|
||||
cd -
|
||||
rm -Rf tmp/
|
||||
|
||||
composer require "php:>=$PHP_VERSION" runtime/frankenphp-symfony
|
||||
composer config --json extra.symfony.docker 'true'
|
||||
|
||||
if grep -q ^DATABASE_URL= .env; then
|
||||
echo "To finish the installation please press Ctrl+C to stop Docker Compose and run: docker compose up --build -d --wait"
|
||||
sleep infinity
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$(ls -A 'vendor/' 2>/dev/null)" ]; then
|
||||
composer install --prefer-dist --no-progress --no-interaction
|
||||
fi
|
||||
|
||||
if grep -q ^DATABASE_URL= .env; then
|
||||
echo "Waiting for database to be ready..."
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=60
|
||||
until [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ] || DATABASE_ERROR=$(php bin/console dbal:run-sql -q "SELECT 1" 2>&1); do
|
||||
if [ $? -eq 255 ]; then
|
||||
# If the Doctrine command exits with 255, an unrecoverable error occurred
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=0
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=$((ATTEMPTS_LEFT_TO_REACH_DATABASE - 1))
|
||||
echo "Still waiting for database to be ready... Or maybe the database is not reachable. $ATTEMPTS_LEFT_TO_REACH_DATABASE attempts left."
|
||||
done
|
||||
|
||||
if [ $ATTEMPTS_LEFT_TO_REACH_DATABASE -eq 0 ]; then
|
||||
echo "The database is not up or not reachable:"
|
||||
echo "$DATABASE_ERROR"
|
||||
exit 1
|
||||
else
|
||||
echo "The database is now ready and reachable"
|
||||
fi
|
||||
|
||||
if [ "$( find ./migrations -iname '*.php' -print -quit )" ]; then
|
||||
php bin/console doctrine:migrations:migrate --no-interaction
|
||||
fi
|
||||
fi
|
||||
|
||||
setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var
|
||||
setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var
|
||||
fi
|
||||
|
||||
exec docker-php-entrypoint "$@"
|
||||
4
frankenphp/worker.Caddyfile
Normal file
4
frankenphp/worker.Caddyfile
Normal file
@@ -0,0 +1,4 @@
|
||||
worker {
|
||||
file ./public/index.php
|
||||
env APP_RUNTIME Runtime\FrankenPhpSymfony\Runtime
|
||||
}
|
||||
0
migrations/.gitignore
vendored
Normal file
0
migrations/.gitignore
vendored
Normal file
24
phpmd.xml
Normal file
24
phpmd.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0"?>
|
||||
<ruleset name="My first PHPMD rule set"
|
||||
xmlns="http://pmd.sf.net/ruleset/1.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0
|
||||
http://pmd.sf.net/ruleset_xml_schema.xsd"
|
||||
xsi:noNamespaceSchemaLocation="
|
||||
http://pmd.sf.net/ruleset_xml_schema.xsd">
|
||||
<description>
|
||||
My custom rule set that checks my code...
|
||||
</description>
|
||||
|
||||
<properties>
|
||||
<property name="cache.enabled" value="true"/>
|
||||
<property name="cache.location" value="var/cache/phpmd.cache"/>
|
||||
</properties>
|
||||
|
||||
<rule ref="rulesets/codesize.xml"/>
|
||||
<rule ref="rulesets/cleancode.xml"/>
|
||||
<rule ref="rulesets/controversial.xml"/>
|
||||
<rule ref="rulesets/design.xml"/>
|
||||
<rule ref="rulesets/naming.xml"/>
|
||||
<rule ref="rulesets/unusedcode.xml"/>
|
||||
</ruleset>
|
||||
26
phpunit.xml.dist
Normal file
26
phpunit.xml.dist
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" backupGlobals="false" colors="true" bootstrap="tests/bootstrap.php" cacheDirectory=".phpunit.cache">
|
||||
<php>
|
||||
<ini name="display_errors" value="1"/>
|
||||
<ini name="error_reporting" value="-1"/>
|
||||
<server name="APP_ENV" value="test" force="true"/>
|
||||
<server name="SHELL_VERBOSITY" value="-1"/>
|
||||
<server name="SYMFONY_PHPUNIT_REMOVE" value=""/>
|
||||
<server name="SYMFONY_PHPUNIT_VERSION" value="9.6"/>
|
||||
<server name="SYMFONY_DEPRECATIONS_HELPER" value="disabled"/>
|
||||
</php>
|
||||
<testsuites>
|
||||
<testsuite name="Project Test Suite">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<extensions>
|
||||
<bootstrap class="Zenstruck\Browser\Test\BrowserExtension"/>
|
||||
</extensions>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
0
public/.gitignore
vendored
Normal file
0
public/.gitignore
vendored
Normal file
9
public/index.php
Normal file
9
public/index.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
return function (array $context) {
|
||||
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
};
|
||||
30
src/ApiPlatform/OpenApiFactoryDecorator.php
Normal file
30
src/ApiPlatform/OpenApiFactoryDecorator.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiPlatform;
|
||||
|
||||
use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
|
||||
use ApiPlatform\OpenApi\Model\SecurityScheme;
|
||||
use ApiPlatform\OpenApi\OpenApi;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
|
||||
|
||||
#[AsDecorator('api_platform.openapi.factory')]
|
||||
class OpenApiFactoryDecorator implements OpenApiFactoryInterface
|
||||
{
|
||||
|
||||
public function __construct(private OpenApiFactoryInterface $decorated)
|
||||
{
|
||||
}
|
||||
|
||||
public function __invoke(array $context = []): OpenApi
|
||||
{
|
||||
$openApi = $this->decorated->__invoke($context);
|
||||
|
||||
$securitySchemes = $openApi->getComponents()->getSecuritySchemes() ?: new \ArrayObject();
|
||||
$securitySchemes['access_token'] = new SecurityScheme(
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
);
|
||||
|
||||
return $openApi;
|
||||
}
|
||||
}
|
||||
0
src/ApiResource/.gitignore
vendored
Normal file
0
src/ApiResource/.gitignore
vendored
Normal file
0
src/Controller/.gitignore
vendored
Normal file
0
src/Controller/.gitignore
vendored
Normal file
37
src/Controller/SecurityController.php
Normal file
37
src/Controller/SecurityController.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use ApiPlatform\Api\IriConverterInterface;
|
||||
use App\Entity\User;
|
||||
use Exception;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||
|
||||
class SecurityController extends AbstractController
|
||||
{
|
||||
#[Route('/login', name: 'app_login', methods: ['GET', 'POST'])]
|
||||
public function login(IriConverterInterface $iriConverter, #[CurrentUser] User $user = null): Response
|
||||
{
|
||||
if(!$user){
|
||||
return $this->json([
|
||||
'error' => 'Invalid credentials'
|
||||
], 401);
|
||||
}
|
||||
|
||||
return new Response(null, 204, [
|
||||
'Location' => $iriConverter->getIriFromResource($user),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Route('/logout', name: 'app_logout', methods: ['GET'])]
|
||||
public function logout(): void
|
||||
{
|
||||
throw new Exception('This method can be blank.');
|
||||
}
|
||||
}
|
||||
86
src/DataFixtures/AppFixtures.php
Normal file
86
src/DataFixtures/AppFixtures.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\DataFixtures;
|
||||
|
||||
use App\Factory\ApiTokenFactory;
|
||||
use App\Factory\CommentFactory;
|
||||
use App\Factory\CompanyFactory;
|
||||
use App\Factory\DocumentFactory;
|
||||
use App\Factory\FolderFactory;
|
||||
use App\Factory\ProjectDocumentFactory;
|
||||
use App\Factory\ProjectFactory;
|
||||
use App\Factory\UserFactory;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
class AppFixtures extends Fixture
|
||||
{
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
CompanyFactory::createMany(5);
|
||||
UserFactory::createMany(20, function () {
|
||||
return [
|
||||
'company' => CompanyFactory::random()
|
||||
];
|
||||
});
|
||||
ApiTokenFactory::createMany(60, function () {
|
||||
return [
|
||||
'ownedBy' => UserFactory::random()
|
||||
];
|
||||
});
|
||||
$projects = ProjectFactory::createMany(100, function () {
|
||||
return [
|
||||
'ownedBy' => UserFactory::random()
|
||||
];
|
||||
});
|
||||
|
||||
$documents = DocumentFactory::createMany(100);
|
||||
|
||||
foreach ($documents as $document) {
|
||||
ProjectDocumentFactory::createMany(3, function () use ($document) {
|
||||
return [
|
||||
'document' => $document,
|
||||
'project' => ProjectFactory::random()
|
||||
];
|
||||
});
|
||||
|
||||
CommentFactory::createMany(1, function () use ($document) {
|
||||
return [
|
||||
'projectDocument' => ProjectDocumentFactory::random(),
|
||||
'postedBy' => UserFactory::random()
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
foreach ($projects as $project) {
|
||||
$projectFolder = FolderFactory::createOne([
|
||||
'label' => 'Root Folder Project ' . $project->getId(),
|
||||
'slug' => 'root-folder-project-' . $project->getId(),
|
||||
'project' => $project,
|
||||
'createdBy' => $project->getOwnedBy()
|
||||
]);
|
||||
$child = FolderFactory::createOne([
|
||||
'label' => 'Subfolder - Parent Folder: ' . $projectFolder->getId(),
|
||||
'slug' => 'subfolder-parent-folder-' . $projectFolder->getId(),
|
||||
'createdBy' => $project->getOwnedBy()
|
||||
]);
|
||||
$grandChild = FolderFactory::createOne([
|
||||
'label' => 'Subfolder - Parent Folder: ' . $child->getId(),
|
||||
'slug' => 'subfolder-parent-folder-' . $child->getId(),
|
||||
'createdBy' => $project->getOwnedBy()
|
||||
]);
|
||||
|
||||
$grandChild->addDocument(DocumentFactory::createOne()->object());
|
||||
$grandChild->addDocument(DocumentFactory::createOne()->object());
|
||||
$grandChild->addDocument(DocumentFactory::createOne()->object());
|
||||
|
||||
$child->addChild(
|
||||
$grandChild->object()
|
||||
);
|
||||
|
||||
$projectFolder->addChild(
|
||||
$child->object()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
0
src/Entity/.gitignore
vendored
Normal file
0
src/Entity/.gitignore
vendored
Normal file
107
src/Entity/ApiToken.php
Normal file
107
src/Entity/ApiToken.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\ApiTokenRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Random\RandomException;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ApiTokenRepository::class)]
|
||||
class ApiToken
|
||||
{
|
||||
|
||||
private const string PERSONAL_ACCESS_TOKEN_PREFIX = 'ipm_';
|
||||
public const string SCOPE_USER_EDIT = 'ROLE_USER_EDIT';
|
||||
public const string SCOPE_PROJECT_CREATE = 'ROLE_PROJECT_CREATE';
|
||||
public const string SCOPE_PROJECT_EDIT = 'ROLE_PROJECT_EDIT';
|
||||
|
||||
public const array SCOPES = [
|
||||
self::SCOPE_USER_EDIT => 'Edit user',
|
||||
self::SCOPE_PROJECT_CREATE => 'Create project',
|
||||
self::SCOPE_PROJECT_EDIT => 'Edit project',
|
||||
];
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'apiTokens')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?User $ownedBy = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?\DateTimeImmutable $expiresAt = null;
|
||||
|
||||
#[ORM\Column(length: 68)]
|
||||
private ?string $token = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private array $scopes = [];
|
||||
|
||||
/**
|
||||
* @throws RandomException
|
||||
*/
|
||||
public function __construct(string $tokenType = self::PERSONAL_ACCESS_TOKEN_PREFIX)
|
||||
{
|
||||
$this->token = $tokenType . bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getOwnedBy(): ?User
|
||||
{
|
||||
return $this->ownedBy;
|
||||
}
|
||||
|
||||
public function setOwnedBy(?User $ownedBy): static
|
||||
{
|
||||
$this->ownedBy = $ownedBy;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getExpiresAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->expiresAt;
|
||||
}
|
||||
|
||||
public function setExpiresAt(?\DateTimeImmutable $expiresAt): static
|
||||
{
|
||||
$this->expiresAt = $expiresAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getToken(): ?string
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function setToken(string $token): static
|
||||
{
|
||||
$this->token = $token;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getScopes(): array
|
||||
{
|
||||
return $this->scopes;
|
||||
}
|
||||
|
||||
public function setScopes(array $scopes): static
|
||||
{
|
||||
$this->scopes = $scopes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->expiresAt === null || $this->expiresAt > new \DateTimeImmutable();
|
||||
}
|
||||
}
|
||||
364
src/Entity/User.php
Normal file
364
src/Entity/User.php
Normal file
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||
#[ORM\Table(name: '`user`')]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(),
|
||||
new GetCollection(),
|
||||
new Post(
|
||||
validationContext: ['groups' => ['Default', 'postValidation']]
|
||||
),
|
||||
new Put(),
|
||||
new Patch(),
|
||||
new Delete()
|
||||
],
|
||||
normalizationContext: ['groups' => ['user:read']],
|
||||
denormalizationContext: ['groups' => ['user:write']],
|
||||
security: "is_granted('ROLE_USER')"
|
||||
)]
|
||||
#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')]
|
||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 180, unique: true)]
|
||||
#[Groups(['user:read', 'user:write'])]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Email]
|
||||
private ?string $email = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private array $roles = [];
|
||||
|
||||
/**
|
||||
* @var ?string The hashed password
|
||||
*/
|
||||
#[ORM\Column]
|
||||
private ?string $password = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Groups(['user:read', 'user:write'])]
|
||||
#[Assert\NotBlank]
|
||||
private ?string $firstName = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Groups(['user:read', 'user:write'])]
|
||||
#[Assert\NotBlank]
|
||||
private ?string $lastName = null;
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'ownedBy', targetEntity: ApiToken::class)]
|
||||
private Collection $apiTokens;
|
||||
|
||||
private ?array $accessTokenScopes = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'employees')]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
#[Groups(['user:read'])]
|
||||
private ?Company $company = null;
|
||||
|
||||
#[Groups(['user:write'])]
|
||||
#[SerializedName('password')]
|
||||
#[Assert\NotBlank(groups: ['postValidation'])]
|
||||
private ?string $plainPassword = null;
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'ownedBy', targetEntity: Project::class)]
|
||||
#[Groups(['user:read'])]
|
||||
private Collection $projects;
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'postedBy', targetEntity: Comment::class)]
|
||||
private Collection $comments;
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'createdBy', targetEntity: Folder::class)]
|
||||
private Collection $folders;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiTokens = new ArrayCollection();
|
||||
$this->projects = new ArrayCollection();
|
||||
$this->comments = new ArrayCollection();
|
||||
$this->folders = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getEmail(): ?string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(string $email): static
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A visual identifier that represents this user.
|
||||
*
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
return (string) $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function getRoles(): array
|
||||
{
|
||||
if($this->accessTokenScopes !== null){
|
||||
$roles = $this->roles;
|
||||
$roles[] = 'ROLE_FRONT_USER';
|
||||
}else{
|
||||
$roles = $this->accessTokenScopes;
|
||||
}
|
||||
|
||||
// guarantee every user at least has ROLE_USER
|
||||
$roles[] = 'ROLE_USER';
|
||||
|
||||
return array_unique($roles);
|
||||
}
|
||||
|
||||
public function setRoles(array $roles): static
|
||||
{
|
||||
$this->roles = $roles;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see PasswordAuthenticatedUserInterface
|
||||
*/
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function setPassword(string $password): static
|
||||
{
|
||||
$this->password = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserInterface
|
||||
*/
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
// If you store any temporary, sensitive data on the user, clear it here
|
||||
$this->plainPassword = null;
|
||||
}
|
||||
|
||||
public function getFirstName(): ?string
|
||||
{
|
||||
return $this->firstName;
|
||||
}
|
||||
|
||||
public function setFirstName(string $firstName): static
|
||||
{
|
||||
$this->firstName = $firstName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLastName(): ?string
|
||||
{
|
||||
return $this->lastName;
|
||||
}
|
||||
|
||||
public function setLastName(string $lastName): static
|
||||
{
|
||||
$this->lastName = $lastName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, ApiToken>
|
||||
*/
|
||||
public function getApiTokens(): Collection
|
||||
{
|
||||
return $this->apiTokens;
|
||||
}
|
||||
|
||||
public function addApiToken(ApiToken $apiToken): static
|
||||
{
|
||||
if (!$this->apiTokens->contains($apiToken)) {
|
||||
$this->apiTokens->add($apiToken);
|
||||
$apiToken->setOwnedBy($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeApiToken(ApiToken $apiToken): static
|
||||
{
|
||||
if ($this->apiTokens->removeElement($apiToken)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($apiToken->getOwnedBy() === $this) {
|
||||
$apiToken->setOwnedBy(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
#[Groups(['user:read'])]
|
||||
public function getValidTokenStrings(): array
|
||||
{
|
||||
return $this->getApiTokens()
|
||||
->filter(fn (ApiToken $token) => $token->isValid())
|
||||
->map(fn (ApiToken $token) => $token->getToken())
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function markAsTokenAuthenticated(array $scopes): void
|
||||
{
|
||||
$this->accessTokenScopes = $scopes;
|
||||
}
|
||||
|
||||
public function getCompany(): ?Company
|
||||
{
|
||||
return $this->company;
|
||||
}
|
||||
|
||||
public function setCompany(?Company $company): static
|
||||
{
|
||||
$this->company = $company;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPlainPassword(): ?string
|
||||
{
|
||||
return $this->plainPassword;
|
||||
}
|
||||
|
||||
public function setPlainPassword(string $plainPassword): void
|
||||
{
|
||||
$this->plainPassword = $plainPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Project>
|
||||
*/
|
||||
public function getProjects(): Collection
|
||||
{
|
||||
return $this->projects;
|
||||
}
|
||||
|
||||
public function addProject(Project $project): static
|
||||
{
|
||||
if (!$this->projects->contains($project)) {
|
||||
$this->projects->add($project);
|
||||
$project->setOwnedBy($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeProject(Project $project): static
|
||||
{
|
||||
if ($this->projects->removeElement($project)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($project->getOwnedBy() === $this) {
|
||||
$project->setOwnedBy(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Comment>
|
||||
*/
|
||||
public function getComments(): Collection
|
||||
{
|
||||
return $this->comments;
|
||||
}
|
||||
|
||||
public function addComment(Comment $comment): static
|
||||
{
|
||||
if (!$this->comments->contains($comment)) {
|
||||
$this->comments->add($comment);
|
||||
$comment->setPostedBy($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeComment(Comment $comment): static
|
||||
{
|
||||
if ($this->comments->removeElement($comment)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($comment->getPostedBy() === $this) {
|
||||
$comment->setPostedBy(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Folder>
|
||||
*/
|
||||
public function getFolders(): Collection
|
||||
{
|
||||
return $this->folders;
|
||||
}
|
||||
|
||||
public function addFolder(Folder $folder): static
|
||||
{
|
||||
if (!$this->folders->contains($folder)) {
|
||||
$this->folders->add($folder);
|
||||
$folder->setCreatedBy($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeFolder(Folder $folder): static
|
||||
{
|
||||
if ($this->folders->removeElement($folder)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($folder->getCreatedBy() === $this) {
|
||||
$folder->setCreatedBy(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
49
src/EventListener/ExceptionListener.php
Normal file
49
src/EventListener/ExceptionListener.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use ApiPlatform\Exception\ItemNotFoundException;
|
||||
use ApiPlatform\Symfony\Validator\Exception\ValidationException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
||||
use ApiPlatform\Exception\FilterValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
|
||||
|
||||
class ExceptionListener
|
||||
{
|
||||
public function __construct(private LoggerInterface $logger)
|
||||
{
|
||||
}
|
||||
|
||||
public function onKernelException(ExceptionEvent $event): void
|
||||
{
|
||||
$exception = $event->getThrowable();
|
||||
|
||||
$response = match(true) {
|
||||
$exception instanceof FilterValidationException,
|
||||
$exception instanceof BadRequestException => $this->createResponse($exception, Response::HTTP_BAD_REQUEST),
|
||||
$exception instanceof NotFoundHttpException,
|
||||
$exception instanceof ItemNotFoundException => $this->createResponse($exception, Response::HTTP_NOT_FOUND),
|
||||
$exception instanceof AccessDeniedHttpException => $this->createResponse($exception, Response::HTTP_FORBIDDEN),
|
||||
$exception instanceof ValidationException,
|
||||
$exception instanceof NotNormalizableValueException => $this->createResponse($exception, Response::HTTP_UNPROCESSABLE_ENTITY),
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($response) {
|
||||
$event->setResponse($response);
|
||||
}else{
|
||||
$this->logger->error($exception->getMessage(), ['exception' => $exception]);
|
||||
}
|
||||
}
|
||||
|
||||
private function createResponse(\Throwable $exception, int $statusCode): Response
|
||||
{
|
||||
$this->logger->info($exception->getMessage(), ['exception' => $exception]);
|
||||
return new Response(json_encode(['message' => $exception->getMessage()]), $statusCode, ['Content-Type' => 'application/json']);
|
||||
}
|
||||
}
|
||||
23
src/EventSubscriber/LogoutSubscriber.php
Normal file
23
src/EventSubscriber/LogoutSubscriber.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Http\Event\LogoutEvent;
|
||||
|
||||
class LogoutSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function onLogoutEvent(LogoutEvent $event): void
|
||||
{
|
||||
$response = new Response(null, 204);
|
||||
$event->setResponse($response);
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
LogoutEvent::class => 'onLogoutEvent',
|
||||
];
|
||||
}
|
||||
}
|
||||
69
src/Factory/ApiTokenFactory.php
Normal file
69
src/Factory/ApiTokenFactory.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Factory;
|
||||
|
||||
use App\Entity\ApiToken;
|
||||
use App\Repository\ApiTokenRepository;
|
||||
use Zenstruck\Foundry\ModelFactory;
|
||||
use Zenstruck\Foundry\Proxy;
|
||||
use Zenstruck\Foundry\RepositoryProxy;
|
||||
|
||||
/**
|
||||
* @extends ModelFactory<ApiToken>
|
||||
*
|
||||
* @method ApiToken|Proxy create(array|callable $attributes = [])
|
||||
* @method static ApiToken|Proxy createOne(array $attributes = [])
|
||||
* @method static ApiToken|Proxy find(object|array|mixed $criteria)
|
||||
* @method static ApiToken|Proxy findOrCreate(array $attributes)
|
||||
* @method static ApiToken|Proxy first(string $sortedField = 'id')
|
||||
* @method static ApiToken|Proxy last(string $sortedField = 'id')
|
||||
* @method static ApiToken|Proxy random(array $attributes = [])
|
||||
* @method static ApiToken|Proxy randomOrCreate(array $attributes = [])
|
||||
* @method static ApiTokenRepository|RepositoryProxy repository()
|
||||
* @method static ApiToken[]|Proxy[] all()
|
||||
* @method static ApiToken[]|Proxy[] createMany(int $number, array|callable $attributes = [])
|
||||
* @method static ApiToken[]|Proxy[] createSequence(iterable|callable $sequence)
|
||||
* @method static ApiToken[]|Proxy[] findBy(array $attributes)
|
||||
* @method static ApiToken[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])
|
||||
* @method static ApiToken[]|Proxy[] randomSet(int $number, array $attributes = [])
|
||||
*/
|
||||
final class ApiTokenFactory extends ModelFactory
|
||||
{
|
||||
/**
|
||||
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
|
||||
*
|
||||
* @todo inject services if required
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
|
||||
*
|
||||
* @todo add your default values here
|
||||
*/
|
||||
protected function getDefaults(): array
|
||||
{
|
||||
return [
|
||||
'ownedBy' => UserFactory::new(),
|
||||
'scopes' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
|
||||
*/
|
||||
protected function initialize(): self
|
||||
{
|
||||
return $this
|
||||
// ->afterInstantiate(function(ApiToken $apiToken): void {})
|
||||
;
|
||||
}
|
||||
|
||||
protected static function getClass(): string
|
||||
{
|
||||
return ApiToken::class;
|
||||
}
|
||||
}
|
||||
80
src/Factory/UserFactory.php
Normal file
80
src/Factory/UserFactory.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Factory;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Zenstruck\Foundry\ModelFactory;
|
||||
use Zenstruck\Foundry\Proxy;
|
||||
use Zenstruck\Foundry\RepositoryProxy;
|
||||
|
||||
/**
|
||||
* @extends ModelFactory<User>
|
||||
*
|
||||
* @method User|Proxy create(array|callable $attributes = [])
|
||||
* @method static User|Proxy createOne(array $attributes = [])
|
||||
* @method static User|Proxy find(object|array|mixed $criteria)
|
||||
* @method static User|Proxy findOrCreate(array $attributes)
|
||||
* @method static User|Proxy first(string $sortedField = 'id')
|
||||
* @method static User|Proxy last(string $sortedField = 'id')
|
||||
* @method static User|Proxy random(array $attributes = [])
|
||||
* @method static User|Proxy randomOrCreate(array $attributes = [])
|
||||
* @method static UserRepository|RepositoryProxy repository()
|
||||
* @method static User[]|Proxy[] all()
|
||||
* @method static User[]|Proxy[] createMany(int $number, array|callable $attributes = [])
|
||||
* @method static User[]|Proxy[] createSequence(iterable|callable $sequence)
|
||||
* @method static User[]|Proxy[] findBy(array $attributes)
|
||||
* @method static User[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])
|
||||
* @method static User[]|Proxy[] randomSet(int $number, array $attributes = [])
|
||||
*/
|
||||
final class UserFactory extends ModelFactory
|
||||
{
|
||||
const array FIRST_NAMES = ["ALAIN", "ALEXANDRE", "ANDRÉ", "ANNIE", "ANTHONY", "AUDREY", "AURÉLIE", "BERNARD", "BRIGITTE", "BRUNO", "CATHERINE", "CEDRIC", "CHANTAL", "CHRISTELLE", "CHRISTIAN", "CHRISTIANE", "CHRISTINE", "CHRISTOPHE", "CLAUDE", "CORINNE", "CÉLINE", "DANIEL", "DANIELLE", "DAVID", "DENISE", "DIDIER", "DOMINIQUE", "ELODIE", "EMILIE", "ENZO", "ERIC", "FABRICE", "FLORENCE", "FRANCK", "FRANÇOISE", "FRÉDÉRIC", "GEORGES", "GERMAINE", "GUILLAUME", "GUY", "GÉRARD", "HENRI", "ISABELLE", "JACQUELINE", "JACQUES", "JEAN", "JEAN-CLAUDE", "JEAN-PIERRE", "JEANNE", "JEANNINE", "JEREMY", "JEROME", "JONATHAN", "JOSEPH", "JULIE", "JULIEN", "KARINE", "KEVIN", "LAETITIA", "LAURA", "LAURENCE", "LAURENT", "LOUIS", "LUCAS", "LÉA", "MADELEINE", "MANON", "MARCEL", "MARCELLE", "MARGUERITE", "MARIE", "MARINE", "MARTINE", "MAURICE", "MAXIME", "MICHEL", "MICHÈLE", "MONIQUE", "NATHALIE", "NICOLAS", "NICOLE", "ODETTE", "OLIVIER", "PASCAL", "PASCALE", "PATRICIA", "PATRICK", "PAUL", "PAULETTE", "PHILIPPE", "PIERRE", "RENÉ", "ROBERT", "ROGER", "ROMAIN", "SANDRA", "SANDRINE", "SERGE", "SOPHIE", "STÉPHANE", "STÉPHANIE", "SUZANNE", "SYLVIE", "SÉBASTIEN", "THIERRY", "THOMAS", "THÉO", "VALÉRIE", "VIRGINIE", "VÉRONIQUE", "YVETTE", "YVONNE"];
|
||||
const array LAST_NAMES = ["Adam", "Andre", "Antoine", "Arnaud", "Aubert", "Aubry", "Bailly", "Barbier", "Baron", "Barre", "Barthelemy", "Benard", "Benoit", "Berger", "Bernard", "Bertin", "Bertrand", "Besson", "Blanc", "Blanchard", "Bonnet", "Boucher", "Bouchet", "Boulanger", "Bourgeois", "Bouvier", "Boyer", "Breton", "Brun", "Brunet", "Carlier", "Caron", "Carpentier", "Carre", "Charles", "Charpentier", "Chauvin", "Chevalier", "Chevallier", "Clement", "Colin", "Collet", "Collin", "Cordier", "Cousin", "Da Silva", "Daniel", "David", "Delaunay", "Denis", "Deschamps", "Dubois", "Dufour", "Dumas", "Dumont", "Dupont", "Dupuis", "Dupuy", "Durand", "Duval", "Etienne", "Fabre", "Faure", "Fernandez", "Fleury", "Fontaine", "Fournier", "Francois", "Gaillard", "Garcia", "Garnier", "Gauthier", "Gautier", "Gay", "Gerard", "Germain", "Gilbert", "Gillet", "Girard", "Giraud", "Gonzalez", "Grondin", "Guerin", "Guichard", "Guillaume", "Guillot", "Guyot", "Hamon", "Henry", "Herve", "Hoarau", "Hubert", "Huet", "Humbert", "Jacob", "Jacquet", "Jean", "Joly", "Julien", "Klein", "Lacroix", "Lambert", "Lamy", "Langlois", "Laporte", "Laurent", "Le Gall", "Le Goff", "Le Roux", "Leblanc", "Lebrun", "Leclerc", "Leclercq", "Lecomte", "Lefebvre", "Lefevre", "Leger", "Legrand", "Lejeune", "Lemaire", "Lemaitre", "Lemoine", "Leroux", "Leroy", "Leveque", "Lopez", "Louis", "Lucas", "Maillard", "Mallet", "Marchal", "Marchand", "Marechal", "Marie", "Martin", "Martinez", "Marty", "Masson", "Mathieu", "Menard", "Mercier", "Meunier", "Meyer", "Michaud", "Michel", "Millet", "Monnier", "Moreau", "Morel", "Morin", "Moulin", "Muller", "Nicolas", "Noel", "Olivier", "Paris", "Pasquier", "Payet", "Pelletier", "Perez", "Perret", "Perrier", "Perrin", "Perrot", "Petit", "Philippe", "Picard", "Pichon", "Pierre", "Poirier", "Poulain", "Prevost", "Remy", "Renard", "Renaud", "Renault", "Rey", "Reynaud", "Richard", "Riviere", "Robert", "Robin", "Roche", "Rodriguez", "Roger", "Rolland", "Rousseau", "Roussel", "Roux", "Roy", "Royer", "Sanchez", "Schmitt", "Schneider", "Simon", "Tessier", "Thomas", "Vasseur", "Vidal", "Vincent", "Weber"];
|
||||
|
||||
/**
|
||||
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
|
||||
*
|
||||
*/
|
||||
public function __construct(private readonly UserPasswordHasherInterface $passwordHasher)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
|
||||
*
|
||||
*/
|
||||
protected function getDefaults(): array
|
||||
{
|
||||
return [
|
||||
'email' => self::faker()->unique()->email(),
|
||||
'password' => 'password',
|
||||
'roles' => [],
|
||||
'firstName' => self::faker()->randomElement(self::FIRST_NAMES),
|
||||
'lastName' => self::faker()->randomElement(self::LAST_NAMES),
|
||||
'company' => CompanyFactory::randomOrCreate(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
|
||||
*/
|
||||
protected function initialize(): self
|
||||
{
|
||||
return $this
|
||||
->afterInstantiate(function(User $user): void {
|
||||
$user->setPassword($this->passwordHasher->hashPassword(
|
||||
$user,
|
||||
$user->getPassword()
|
||||
));
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
protected static function getClass(): string
|
||||
{
|
||||
return User::class;
|
||||
}
|
||||
}
|
||||
35
src/Kernel.php
Normal file
35
src/Kernel.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Override;
|
||||
use Doctrine\ORM\Mapping\ClassMetadataInfo;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||
|
||||
class Kernel extends BaseKernel
|
||||
{
|
||||
use MicroKernelTrait;
|
||||
|
||||
#[Override]
|
||||
protected function build(ContainerBuilder $container): void
|
||||
{
|
||||
$container->addCompilerPass(new class() implements CompilerPassInterface {
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
$container->getDefinition('doctrine.orm.default_configuration')
|
||||
->addMethodCall(
|
||||
'setIdentityGenerationPreferences',
|
||||
[
|
||||
[
|
||||
PostgreSQLPlatform::class => ClassMetadataInfo::GENERATOR_TYPE_SEQUENCE,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
0
src/Repository/.gitignore
vendored
Normal file
0
src/Repository/.gitignore
vendored
Normal file
48
src/Repository/ApiTokenRepository.php
Normal file
48
src/Repository/ApiTokenRepository.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\ApiToken;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<ApiToken>
|
||||
*
|
||||
* @method ApiToken|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method ApiToken|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method ApiToken[] findAll()
|
||||
* @method ApiToken[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class ApiTokenRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, ApiToken::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return ApiToken[] Returns an array of ApiToken objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('a')
|
||||
// ->andWhere('a.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('a.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?ApiToken
|
||||
// {
|
||||
// return $this->createQueryBuilder('a')
|
||||
// ->andWhere('a.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
67
src/Repository/UserRepository.php
Normal file
67
src/Repository/UserRepository.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<User>
|
||||
*
|
||||
* @implements PasswordUpgraderInterface<User>
|
||||
*
|
||||
* @method User|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method User|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method User[] findAll()
|
||||
* @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to upgrade (rehash) the user's password automatically over time.
|
||||
*/
|
||||
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
|
||||
{
|
||||
if (!$user instanceof User) {
|
||||
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class));
|
||||
}
|
||||
|
||||
$user->setPassword($newHashedPassword);
|
||||
$this->getEntityManager()->persist($user);
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return User[] Returns an array of User objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('u')
|
||||
// ->andWhere('u.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('u.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?User
|
||||
// {
|
||||
// return $this->createQueryBuilder('u')
|
||||
// ->andWhere('u.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
35
src/Security/ApiTokenHandler.php
Normal file
35
src/Security/ApiTokenHandler.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Repository\ApiTokenRepository;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
|
||||
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
|
||||
readonly class ApiTokenHandler implements AccessTokenHandlerInterface
|
||||
{
|
||||
|
||||
public function __construct(private ApiTokenRepository $apiTokenRepository)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge
|
||||
{
|
||||
$token = $this->apiTokenRepository->findOneBy(['token' => $accessToken]);
|
||||
|
||||
if(!$token) {
|
||||
throw new BadCredentialsException();
|
||||
}
|
||||
|
||||
if(!$token->isValid()){
|
||||
throw new CustomUserMessageAuthenticationException('Token expired');
|
||||
}
|
||||
|
||||
$token->getOwnedBy()->markAsTokenAuthenticated($token->getScopes());
|
||||
|
||||
return new UserBadge($token->getOwnedBy()->getUserIdentifier());
|
||||
}
|
||||
}
|
||||
26
src/State/UserHashPasswordProcessor.php
Normal file
26
src/State/UserHashPasswordProcessor.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
|
||||
#[AsDecorator('api_platform.doctrine.orm.state.persist_processor')]
|
||||
readonly class UserHashPasswordProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(private ProcessorInterface $decoratedProcessor, private UserPasswordHasherInterface $passwordHasher)
|
||||
{
|
||||
}
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
||||
{
|
||||
if($data instanceof User && $data->getPlainPassword() !== null) {
|
||||
$data->setPassword($this->passwordHasher->hashPassword($data, $data->getPlainPassword()));
|
||||
}
|
||||
|
||||
$this->decoratedProcessor->process($data, $operation, $uriVariables, $context);
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
248
symfony.lock
Normal file
248
symfony.lock
Normal file
@@ -0,0 +1,248 @@
|
||||
{
|
||||
"api-platform/core": {
|
||||
"version": "3.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "3.2",
|
||||
"ref": "696d44adc3c0d4f5d25a2f1c4f3700dd8a5c6db9"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/api_platform.yaml",
|
||||
"config/routes/api_platform.yaml",
|
||||
"src/ApiResource/.gitignore"
|
||||
]
|
||||
},
|
||||
"doctrine/doctrine-bundle": {
|
||||
"version": "2.11",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.10",
|
||||
"ref": "310a02a22033e35640468f48ff6bf31a25891537"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/doctrine.yaml",
|
||||
"src/Entity/.gitignore",
|
||||
"src/Repository/.gitignore"
|
||||
]
|
||||
},
|
||||
"doctrine/doctrine-fixtures-bundle": {
|
||||
"version": "3.5",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "3.0",
|
||||
"ref": "1f5514cfa15b947298df4d771e694e578d4c204d"
|
||||
},
|
||||
"files": [
|
||||
"src/DataFixtures/AppFixtures.php"
|
||||
]
|
||||
},
|
||||
"doctrine/doctrine-migrations-bundle": {
|
||||
"version": "3.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "3.1",
|
||||
"ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/doctrine_migrations.yaml",
|
||||
"migrations/.gitignore"
|
||||
]
|
||||
},
|
||||
"friendsofphp/php-cs-fixer": {
|
||||
"version": "3.48",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "3.0",
|
||||
"ref": "be2103eb4a20942e28a6dd87736669b757132435"
|
||||
},
|
||||
"files": [
|
||||
".php-cs-fixer.dist.php"
|
||||
]
|
||||
},
|
||||
"nelmio/cors-bundle": {
|
||||
"version": "2.4",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "1.5",
|
||||
"ref": "6bea22e6c564fba3a1391615cada1437d0bde39c"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/nelmio_cors.yaml"
|
||||
]
|
||||
},
|
||||
"phpunit/phpunit": {
|
||||
"version": "10.5",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "9.6",
|
||||
"ref": "7364a21d87e658eb363c5020c072ecfdc12e2326"
|
||||
},
|
||||
"files": [
|
||||
".env.test",
|
||||
"phpunit.xml.dist",
|
||||
"tests/bootstrap.php"
|
||||
]
|
||||
},
|
||||
"symfony/console": {
|
||||
"version": "7.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "5.3",
|
||||
"ref": "da0c8be8157600ad34f10ff0c9cc91232522e047"
|
||||
},
|
||||
"files": [
|
||||
"bin/console"
|
||||
]
|
||||
},
|
||||
"symfony/flex": {
|
||||
"version": "2.4",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "146251ae39e06a95be0fe3d13c807bcf3938b172"
|
||||
},
|
||||
"files": [
|
||||
".env"
|
||||
]
|
||||
},
|
||||
"symfony/framework-bundle": {
|
||||
"version": "7.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.0",
|
||||
"ref": "6356c19b9ae08e7763e4ba2d9ae63043efc75db5"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/cache.yaml",
|
||||
"config/packages/framework.yaml",
|
||||
"config/preload.php",
|
||||
"config/routes/framework.yaml",
|
||||
"config/services.yaml",
|
||||
"public/index.php",
|
||||
"src/Controller/.gitignore",
|
||||
"src/Kernel.php"
|
||||
]
|
||||
},
|
||||
"symfony/maker-bundle": {
|
||||
"version": "1.52",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
|
||||
}
|
||||
},
|
||||
"symfony/monolog-bundle": {
|
||||
"version": "3.10",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "3.7",
|
||||
"ref": "aff23899c4440dd995907613c1dd709b6f59503f"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/monolog.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/phpunit-bridge": {
|
||||
"version": "7.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.3",
|
||||
"ref": "a411a0480041243d97382cac7984f7dce7813c08"
|
||||
},
|
||||
"files": [
|
||||
".env.test",
|
||||
"bin/phpunit",
|
||||
"phpunit.xml.dist",
|
||||
"tests/bootstrap.php"
|
||||
]
|
||||
},
|
||||
"symfony/routing": {
|
||||
"version": "7.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.0",
|
||||
"ref": "21b72649d5622d8f7da329ffb5afb232a023619d"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/routing.yaml",
|
||||
"config/routes.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/security-bundle": {
|
||||
"version": "7.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.4",
|
||||
"ref": "2ae08430db28c8eb4476605894296c82a642028f"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/security.yaml",
|
||||
"config/routes/security.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/twig-bundle": {
|
||||
"version": "7.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.4",
|
||||
"ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/twig.yaml",
|
||||
"templates/base.html.twig"
|
||||
]
|
||||
},
|
||||
"symfony/validator": {
|
||||
"version": "7.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.0",
|
||||
"ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/validator.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/web-profiler-bundle": {
|
||||
"version": "7.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.1",
|
||||
"ref": "e42b3f0177df239add25373083a564e5ead4e13a"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/web_profiler.yaml",
|
||||
"config/routes/web_profiler.yaml"
|
||||
]
|
||||
},
|
||||
"zenstruck/foundry": {
|
||||
"version": "1.36",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "1.10",
|
||||
"ref": "37c2f894cc098ab4c08874b80cccc8e2f8de7976"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/zenstruck_foundry.yaml"
|
||||
]
|
||||
}
|
||||
}
|
||||
16
templates/base.html.twig
Normal file
16
templates/base.html.twig
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Welcome!{% endblock %}</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
|
||||
{% block stylesheets %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
26
tests/Functional/ApiTestCase.php
Normal file
26
tests/Functional/ApiTestCase.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Functional;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Zenstruck\Browser\HttpOptions;
|
||||
use Zenstruck\Browser\KernelBrowser;
|
||||
use Zenstruck\Browser\Test\HasBrowser;
|
||||
use Zenstruck\Foundry\Test\Factories;
|
||||
use Zenstruck\Foundry\Test\ResetDatabase;
|
||||
|
||||
abstract class ApiTestCase extends KernelTestCase
|
||||
{
|
||||
use ResetDatabase, Factories, HasBrowser {
|
||||
browser as baseKernelBrowser;
|
||||
}
|
||||
|
||||
protected function browser(array $options = [], array $server = []): KernelBrowser
|
||||
{
|
||||
return $this->baseKernelBrowser($options, $server)
|
||||
->setDefaultHttpOptions(
|
||||
HttpOptions::create()
|
||||
->withHeader('Accept', 'application/ld+json')
|
||||
);
|
||||
}
|
||||
}
|
||||
112
tests/Functional/UserResourceTest.php
Normal file
112
tests/Functional/UserResourceTest.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Functional;
|
||||
|
||||
use App\Factory\ApiTokenFactory;
|
||||
use App\Factory\CompanyFactory;
|
||||
use App\Factory\UserFactory;
|
||||
|
||||
class UserResourceTest extends ApiTestCase
|
||||
{
|
||||
public function testUserLoginHttp(): void
|
||||
{
|
||||
$company = CompanyFactory::createOne();
|
||||
$user = UserFactory::createOne(['company' => $company]);
|
||||
|
||||
$this->browser()
|
||||
->post('/login', [
|
||||
'json' => [
|
||||
'email' => $user->getEmail(),
|
||||
'password' => 'password'
|
||||
]
|
||||
])
|
||||
->assertStatus(204)
|
||||
->assertHeaderContains('Location', '/api/users/' . $user->getId());
|
||||
}
|
||||
|
||||
public function testUserLogoutHttp()
|
||||
{
|
||||
$user = UserFactory::createOne();
|
||||
$this->browser()
|
||||
->actingAs($user)
|
||||
->get('/logout')
|
||||
->assertStatus(204)
|
||||
;
|
||||
}
|
||||
|
||||
public function testUserLoginToken(): void
|
||||
{
|
||||
$token = ApiTokenFactory::createOne();
|
||||
|
||||
$this->browser()
|
||||
->get('api/users', [
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . $token->getToken()
|
||||
]
|
||||
])
|
||||
->assertStatus(200);
|
||||
}
|
||||
|
||||
public function testCanGetUser(): void
|
||||
{
|
||||
$user = UserFactory::createOne();
|
||||
|
||||
$this->browser()
|
||||
->actingAs($user)
|
||||
->get('/api/users/' . $user->getId())
|
||||
->assertSuccessful()
|
||||
->assertJson()
|
||||
->assertJsonMatches('email', $user->getEmail())
|
||||
->assertJsonMatches('firstName', $user->getFirstName())
|
||||
->assertJsonMatches('lastName', $user->getLastName())
|
||||
;
|
||||
}
|
||||
|
||||
public function testCanPostToCreateUser(): void
|
||||
{
|
||||
$loggedUser = UserFactory::createOne();
|
||||
|
||||
$this->browser()
|
||||
->actingAs($loggedUser)
|
||||
->post('/api/users', [
|
||||
'json' => [
|
||||
'email' => 'john.doe@mail.com',
|
||||
'firstName' => 'John',
|
||||
'lastName' => 'Doe',
|
||||
'password' => 'password',
|
||||
],
|
||||
])
|
||||
->assertSuccessful()
|
||||
->post('/login', [
|
||||
'json' => [
|
||||
'email' => 'john.doe@mail.com',
|
||||
'password' => 'password',
|
||||
],
|
||||
])
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
public function testCanPatchToUpdateUser(): void
|
||||
{
|
||||
$loggedUser = UserFactory::createOne();
|
||||
|
||||
$this->browser()
|
||||
->actingAs($loggedUser)
|
||||
->patch('/api/users/' . $loggedUser->getId(), [
|
||||
'json' => [
|
||||
'firstName' => 'John',
|
||||
'lastName' => 'Doe',
|
||||
],
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/merge-patch+json'
|
||||
]
|
||||
])
|
||||
->assertSuccessful()
|
||||
->get('/api/users/' . $loggedUser->getId())
|
||||
->assertSuccessful()
|
||||
->assertJson()
|
||||
->assertJsonMatches('firstName', 'John')
|
||||
->assertJsonMatches('lastName', 'Doe');
|
||||
;
|
||||
}
|
||||
}
|
||||
13
tests/bootstrap.php
Normal file
13
tests/bootstrap.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
|
||||
require dirname(__DIR__).'/vendor/autoload.php';
|
||||
|
||||
if (method_exists(Dotenv::class, 'bootEnv')) {
|
||||
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
|
||||
}
|
||||
|
||||
if ($_SERVER['APP_DEBUG']) {
|
||||
umask(0000);
|
||||
}
|
||||
Reference in New Issue
Block a user