diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..c89fca5
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,80 @@
+{
+ "name": "philippe-vandermoere/skeleton",
+ "type": "project",
+ "license": "MIT",
+ "minimum-stability": "stable",
+ "require": {
+ "php": "^7.4",
+ "ext-ctype": "*",
+ "ext-iconv": "*",
+ "ext-json": "*",
+ "symfony/flex": "^1.6"
+ },
+ "flex-require": {
+ "symfony/console": "*",
+ "symfony/dotenv": "*",
+ "symfony/expression-language": "*",
+ "symfony/framework-bundle": "*",
+ "symfony/http-client": "*",
+ "symfony/monolog-bundle": "^3.5",
+ "symfony/orm-pack": "^1.0",
+ "symfony/yaml": "*"
+ },
+ "flex-require-dev": {
+ "phpstan/phpstan": "^0.12",
+ "phpstan/phpstan-doctrine": "^0.12",
+ "phpstan/phpstan-phpunit": "^0.12",
+ "phpstan/phpstan-symfony": "^0.12",
+ "phpunit/phpunit": "^8.5",
+ "squizlabs/php_codesniffer": "^3.5",
+ "symfony/maker-bundle": "^1.0"
+ },
+ "config": {
+ "optimize-autoloader": true,
+ "preferred-install": {
+ "*": "dist"
+ },
+ "sort-packages": true
+ },
+ "autoload": {
+ "psr-4": {
+ "App\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "App\\Tests\\": "tests/"
+ }
+ },
+ "replace": {
+ "paragonie/random_compat": "2.*",
+ "symfony/polyfill-ctype": "*",
+ "symfony/polyfill-iconv": "*",
+ "symfony/polyfill-php72": "*",
+ "symfony/polyfill-php71": "*",
+ "symfony/polyfill-php70": "*",
+ "symfony/polyfill-php56": "*"
+ },
+ "scripts": {
+ "auto-scripts":[
+ ],
+ "post-install-cmd": [
+ "@auto-scripts"
+ ],
+ "post-update-cmd": [
+ "@auto-scripts"
+ ],
+ "post-create-project-cmd": [
+ "./post_create_project.sh"
+ ]
+ },
+ "conflict": {
+ "symfony/symfony": "*"
+ },
+ "extra": {
+ "symfony": {
+ "allow-contrib": true,
+ "require": "^5.0"
+ }
+ }
+}
diff --git a/post_create_project.sh b/post_create_project.sh
new file mode 100755
index 0000000..7bc2fa3
--- /dev/null
+++ b/post_create_project.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+
+set -e
+
+readonly GREEN='\e[0;32m'
+readonly RESET='\e[0m'
+
+readonly PROJECT_FOLDER=$(realpath "$(dirname "$0")")
+readonly PROJECT_NAME=$(basename "${PROJECT_FOLDER}")
+readonly PROJECT_DEV_URL="${PROJECT_NAME,,}.philou.dev"
+
+cp -rpf "${PROJECT_FOLDER}/skeleton/". "${PROJECT_FOLDER}/"
+
+rm -f "${PROJECT_FOLDER}/config/services.yaml"
+rm -f "${PROJECT_FOLDER}/config/routes.yaml"
+
+echo -e "${GREEN}Update config files${RESET}"
+for file in README.md docs/DOCKER.md docker/dev/.env.dist; do
+ if [[ -w "${PROJECT_FOLDER}/${file}" ]]; then
+ sed s/skeleton_name/${PROJECT_NAME}/g -i "${PROJECT_FOLDER}/${file}"
+ sed s/skeleton_url/${PROJECT_DEV_URL}/g -i "${PROJECT_FOLDER}/${file}"
+ fi
+done
+
+echo -e "${GREEN}Remove post-create-project-cmd in composer.json${RESET}"
+readonly TMP_COMPOSER_FILE=$(mktemp)
+mv "${PROJECT_FOLDER}/composer.json" "${TMP_COMPOSER_FILE}"
+cat "${TMP_COMPOSER_FILE}" | jq 'del(.scripts."post-create-project-cmd")' --indent 4 > "${PROJECT_FOLDER}/composer.json"
+rm -f "${TMP_COMPOSER_FILE}"
+
+echo -e "${GREEN}Remove Skeleton files${RESET}"
+rm -rf "${PROJECT_FOLDER}/skeleton"
+rm -f "$0"
+
+echo -e "${GREEN}Update .gitignore${RESET}"
+echo '/.idea' >> "${PROJECT_FOLDER}/.gitignore"
+echo '/docker/.env' >> "${PROJECT_FOLDER}/.gitignore"
+
+echo -e "${GREEN}Initialize GIT repository for ${PROJECT_NAME^}${RESET}"
+cd ${PROJECT_FOLDER}
+git init -q
+git add .
+git commit -m "bootstrap project ${PROJECT_NAME}" -q
diff --git a/skeleton/.circleci/config.yml b/skeleton/.circleci/config.yml
new file mode 100644
index 0000000..5bcccd4
--- /dev/null
+++ b/skeleton/.circleci/config.yml
@@ -0,0 +1,96 @@
+version: '2.1'
+commands:
+ alpine_checkout:
+ description: Optimize Alpine checkout.
+ steps:
+ - run:
+ name: Install alpine requirements
+ command: apk add git openssh-client curl make
+ - checkout
+
+executors:
+ php:
+ docker:
+ - image: php:7.4-cli-alpine
+
+jobs:
+ vendor:
+ executor: php
+ working_directory: ~/repo
+ steps:
+ - alpine_checkout
+ - restore_cache:
+ key: vendor-{{ checksum "composer.json" }}-{{ checksum "composer.lock" }}
+ - run:
+ name: composer
+ command: |
+ if [[ ! -f vendor/autoload.php ]]; then
+ curl --location --silent https://getcomposer.org/composer.phar -o /usr/bin/composer; \
+ chmod +x /usr/bin/composer; \
+ composer global require hirak/prestissimo; \
+ composer install --no-progress --no-interaction; \
+ fi
+ - save_cache:
+ key: vendor-{{ checksum "composer.json" }}-{{ checksum "composer.lock" }}
+ paths:
+ - vendor
+ - persist_to_workspace:
+ root: .
+ paths:
+ - vendor
+
+ phpcs:
+ executor: php
+ working_directory: ~/repo
+ steps:
+ - alpine_checkout
+ - attach_workspace:
+ at: .
+ - run:
+ name: phpcs
+ command: make phpcs
+
+ phpstan:
+ executor: php
+ working_directory: ~/repo
+ steps:
+ - alpine_checkout
+ - attach_workspace:
+ at: .
+ - run:
+ name: phpstan
+ command: make phpstan
+
+ phpunit:
+ executor: php
+ working_directory: ~/repo
+ steps:
+ - alpine_checkout
+ - attach_workspace:
+ at: .
+ - run:
+ name: phpunit
+ command: make phpunit options="--log-junit ~/phpunit/junit.xml --coverage-html ~/coverage-html"
+ - store_artifacts:
+ path: ~/coverage-html
+ destination: coverage-html
+ - store_artifacts:
+ path: ~/phpunit
+ destination: phpunit
+ - store_test_results:
+ path: ~/phpunit
+
+workflows:
+ version: '2.1'
+ tests:
+ jobs:
+ - vendor
+ - phpcs:
+ requires:
+ - vendor
+ - phpstan:
+ requires:
+ - vendor
+ - phpunit:
+ requires:
+ - vendor
diff --git a/skeleton/.editorconfig b/skeleton/.editorconfig
new file mode 100755
index 0000000..1791e19
--- /dev/null
+++ b/skeleton/.editorconfig
@@ -0,0 +1,12 @@
+[*]
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8
+
+[Makefile]
+indent_style = tab
+
+[*.{php,less,js,yml,yaml}]
+indent_style = space
+indent_size = 4
diff --git a/skeleton/LICENSE b/skeleton/LICENSE
new file mode 100644
index 0000000..4b99069
--- /dev/null
+++ b/skeleton/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2020 Philippe VANDERMOERE
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/skeleton/Makefile b/skeleton/Makefile
new file mode 100644
index 0000000..8886732
--- /dev/null
+++ b/skeleton/Makefile
@@ -0,0 +1,81 @@
+ifndef VERBOSE
+ MAKEFLAGS += --no-print-directory
+endif
+
+default:
+
+phpcs:
+ vendor/bin/phpcs
+
+phpstan:
+ APP_ENV=test bin/console cache:clear
+ vendor/bin/phpstan analyse
+
+phpunit:
+ phpdbg -qrr vendor/bin/phpunit $(options)
+
+tests: phpcs phpstan phpunit
+
+install:
+ composer install
+ bin/console doctrine:database:create --no-interaction --if-not-exists
+ bin/console doctrine:migrations:migrate --no-interaction --query-time --allow-no-migration
+
+.out_docker:
+ifeq (, $(shell which docker))
+ $(error "You must run this command outside the docker container. ")
+endif
+
+start: .out_docker
+ $(shell docker/dev/configure.sh)
+ @cd docker/dev && docker-compose build --pull --parallel --quiet
+ @cd docker/dev && docker-compose up --detach --remove-orphans --quiet-pull --scale nginx=2 --scale php-fpm=2
+ @cd docker/dev && docker-compose exec php-fpm make .waiting_for_dependency
+ @cd docker/dev && docker-compose exec php-fpm make install
+
+.waiting_for_dependency:
+ @make .waiting_for service=mysql port=3306 timeout=30
+
+.waiting_for:
+ @echo -e "\e[1;33mWaiting for $(service) is Ready\e[0m"
+ @/bin/sh -c 'for i in `seq 1 $(timeout)`;do nc $(service) $(port) -w 1 -z && exit 0;sleep 1;done;exit 1'
+ @echo -e "\e[1;32m$(service) is ready\e[0m"
+
+stop: .out_docker
+ @cd docker/dev && docker-compose stop
+
+remove: .out_docker
+ @cd docker/dev && docker-compose down --remove-orphans --volumes
+
+restart: .out_docker
+ @cd docker/dev && docker-compose restart
+
+ps: .out_docker
+ @cd docker/dev && docker-compose ps
+
+shell: .out_docker
+ @cd docker/dev && docker-compose exec php-fpm /bin/bash
+
+mysql_cli: .out_docker
+ @cd docker/dev && docker-compose exec mysql /bin/bash -c 'mysql -uroot -p$${MYSQL_ROOT_PASSWORD} $${MYSQL_DATABASE}'
+
+redis_cli: .out_docker
+ @cd docker/dev && docker-compose exec redis redis-cli
+
+logs: .out_docker
+ @cd docker/dev && docker-compose logs --timestamps --follow --tail=50 $(service)
+
+logs_php-nginx:
+ @make logs service=nginx
+
+logs_php-fpm:
+ @make logs service=php-fpm
+
+logs_mysql:
+ @make logs service=mysql
+
+logs_redis:
+ @make logs service=redis
+
+logs_rabbitmq:
+ @make logs service=rabbitmq
diff --git a/skeleton/README.md b/skeleton/README.md
new file mode 100644
index 0000000..3ce91de
--- /dev/null
+++ b/skeleton/README.md
@@ -0,0 +1,36 @@
+# skeleton_name
+
+## Development
+
+### Installation
+
+- [with Docker](docs/DOCKER.md) (recommended)
+
+### Tests
+
+Run all tests:
+- PHPCs
+- PHPStan
+- PHPUnit
+
+```bash
+make tests
+```
+
+Run PHPCs tests:
+
+```bash
+make phpcs
+```
+
+Run PHPStan tests:
+
+```bash
+make phpstan
+```
+
+Run PHPUnit tests:
+
+```bash
+make phpunit
+```
diff --git a/skeleton/config/services/controller.yaml b/skeleton/config/services/controller.yaml
new file mode 100644
index 0000000..13562ef
--- /dev/null
+++ b/skeleton/config/services/controller.yaml
@@ -0,0 +1,8 @@
+services:
+ _defaults:
+ autowire: true
+ autoconfigure: true
+
+ App\Controller\:
+ resource: '../../src/Controller'
+ tags: ['controller.service_arguments']
diff --git a/skeleton/docker/dev/.env.dist b/skeleton/docker/dev/.env.dist
new file mode 100644
index 0000000..cb4d211
--- /dev/null
+++ b/skeleton/docker/dev/.env.dist
@@ -0,0 +1,13 @@
+COMPOSE_PROJECT_NAME=skeleton_name
+HTTP_HOST=skeleton_url
+DOCKER_UID=
+GITHUB_TOKEN=
+GITHUB_CERTIFICATES_REPOSITORY=
+TIMEZONE=Europe/Paris
+MYSQL_ROOT_PASSWORD=root
+MYSQL_DATABASE=test
+MYSQL_USER=test
+MYSQL_PASSWORD=test
+AMQP_USER=test
+AMQP_PASSWORD=test
+AMQP_VHOST=/
diff --git a/skeleton/docker/dev/configure.sh b/skeleton/docker/dev/configure.sh
new file mode 100755
index 0000000..b40ed7a
--- /dev/null
+++ b/skeleton/docker/dev/configure.sh
@@ -0,0 +1,112 @@
+#!/usr/bin/env bash
+
+set -e
+
+# PROMPT COLOURS
+readonly RESET='\033[0;0m'
+readonly BLACK='\033[0;30m'
+readonly RED='\033[0;31m'
+readonly GREEN='\033[0;32m'
+readonly YELLOW='\033[0;33m'
+readonly BLUE='\033[0;34m'
+readonly PURPLE='\033[0;35m'
+readonly CYAN='\033[0;36m'
+readonly WHITE='\033[0;37m'
+
+function ask_value() {
+ local message=$1
+ local default_value=$2
+ local count=${3:-0}
+ local value
+ local default_value_message=''
+
+ if [[ ${count} -ge 3 ]]; then
+ exit 1
+ fi
+
+ if [[ -n ${default_value} ]]; then
+ default_value_message=" (default: ${YELLOW}${default_value}${CYAN})"
+ fi
+
+ echo -e "${CYAN}${message}${default_value_message}: ${RESET}" > /dev/tty
+ read -r value < /dev/tty
+
+ if [[ -z "${value}" ]]; then
+ if [[ -z ${default_value} ]]; then
+ value=$(ask_value "${message}" '' $(( count +1 )))
+ else
+ value="${default_value}"
+ fi
+ fi
+
+ echo "${value}"
+}
+
+function add_host() {
+ local host=$1
+
+ if [[ $(grep -c "${host}" /etc/hosts ) -eq 0 ]]; then
+ sudo /bin/bash -c "echo \"127.0.0.1 ${host}\" >> /etc/hosts"
+ fi
+}
+
+function get_compute_env_value() {
+ local key=$1
+ local default_value=$2
+ local value
+
+ case ${key} in
+ COMPOSE_PROJECT_NAME)
+ value="${default_value}"
+ ;;
+ HTTP_HOST)
+ value=${default_value}
+ add_host "${value}"
+ ;;
+ DOCKER_UID)
+ value=$(id -u)
+ ;;
+ esac
+
+ echo "${value}"
+}
+
+function configure_env_value() {
+ local env_file=$1
+ local key=$2
+ local default_value=$3
+ local value
+
+ if [[ ! -f "${env_file}" ]]; then
+ touch "${env_file}"
+ fi
+
+ value=$(get_compute_env_value "${key}" "${default_value}")
+
+ if [[ -z "${value}" ]]; then
+ if [[ $(grep -Ec "^${key}=" "${env_file}") -eq 0 ]]; then
+ value=$(ask_value "Define the value of ${key}" "${default_value}")
+
+ if [[ -z "${value}" ]]; then
+ echo -e "${RED}No value provide for key ${key}.${RESET}" > /dev/tty
+ exit 1
+ fi
+
+ else
+ value=$(awk -F "${key} *= *" '{print $2}' "${env_file}")
+ fi
+ fi
+
+ sed -e "/^${key}=/d" -i "${env_file}"
+ echo "${key}=${value}" >> "${env_file}"
+}
+
+cd "$(dirname "$0")"
+
+while read -r line; do
+ if [[ -z "${line}" ]]; then
+ continue
+ fi
+
+ configure_env_value .env "${line%%=*}" "${line#*=}"
+done < .env.dist
diff --git a/skeleton/docker/dev/docker-compose.yml b/skeleton/docker/dev/docker-compose.yml
new file mode 100644
index 0000000..ebf37b5
--- /dev/null
+++ b/skeleton/docker/dev/docker-compose.yml
@@ -0,0 +1,73 @@
+version: '3.7'
+services:
+ nginx:
+ labels: &label-docker-proxy
+ com.docker-proxy.domain: ${HTTP_HOST}
+ com.docker-proxy.ssl: true
+ com.docker-proxy.certificate-provider.name: github
+ com.docker-proxy.certificate-provider.token: ${GITHUB_TOKEN}
+ com.docker-proxy.certificate-provider.repository: ${GITHUB_CERTIFICATES_REPOSITORY}
+ com.docker-proxy.certificate-provider.certificate_path: ${HTTP_HOST}/fullchain.pem
+ com.docker-proxy.certificate-provider.private_key_path: ${HTTP_HOST}/privkey.pem
+ build:
+ context: nginx
+ environment:
+ PHP_FPM_UPSTREAM: php-fpm:9000
+ DNS_RESOLVER: 127.0.0.11
+ volumes:
+ - ./../../public:/var/www/html/public
+
+ php-fpm: &php-service
+ hostname: ${COMPOSE_PROJECT_NAME}-php-fpm
+ build:
+ context: php
+ args:
+ DOCKER_UID: ${DOCKER_UID}
+ environment:
+ DATABASE_URL: mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DATABASE}?serverVersion=8.0&charset=utf8
+ TIMEZONE: ${TIMEZONE}
+ volumes:
+ - ./../..:/var/www/html
+ tmpfs:
+ - /var/www/html/var/cache:uid=${DOCKER_UID},gid=82
+
+ mysql:
+ image: mysql:8.0
+ environment:
+ MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
+ MYSQL_DATABASE: ${MYSQL_DATABASE}
+ MYSQL_USER: ${MYSQL_USER}
+ MYSQL_PASSWORD: ${MYSQL_PASSWORD}
+ TZ: ${TIMEZONE}
+ volumes:
+ - mysql:/var/lib/mysql
+
+ adminer:
+ labels:
+ <<: *label-docker-proxy
+ com.docker-proxy.port: 8080
+ com.docker-proxy.path: /adminer
+ image: adminer:4.7
+ environment:
+ ADMINER_DEFAULT_SERVER: mysql
+
+ redis:
+ image: redis:5.0-alpine
+
+ rabbitmq:
+ labels:
+ <<: *label-docker-proxy
+ com.docker-proxy.port: 15672
+ com.docker-proxy.path: /rabbitmq
+ image: rabbitmq:3.8-management-alpine
+ environment:
+ RABBITMQ_DEFAULT_USER: ${AMQP_USER}
+ RABBITMQ_DEFAULT_PASS: ${AMQP_PASSWORD}
+ RABBITMQ_DEFAULT_VHOST: ${AMQP_VHOST}
+ RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS: -rabbitmq_management path_prefix "/rabbitmq"
+ volumes:
+ - rabbitmq:/var/lib/rabbitmq
+
+volumes:
+ mysql:
+ rabbitmq:
diff --git a/skeleton/docker/dev/nginx/Dockerfile b/skeleton/docker/dev/nginx/Dockerfile
new file mode 100644
index 0000000..a4c1349
--- /dev/null
+++ b/skeleton/docker/dev/nginx/Dockerfile
@@ -0,0 +1,11 @@
+FROM nginx:1.16-alpine
+
+ENV PHP_FPM_UPSTREAM=php:9000 \
+ DNS_RESOLVER=127.0.0.11
+
+COPY config/vhost.conf /etc/nginx/conf.d/vhost.template
+COPY config/nginx.conf /etc/nginx/nginx.conf
+
+WORKDIR /var/www/html
+
+CMD ["/bin/sh", "-c", "envsubst '$PHP_FPM_UPSTREAM,$DNS_RESOLVER' < /etc/nginx/conf.d/vhost.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"]
diff --git a/skeleton/docker/dev/nginx/config/nginx.conf b/skeleton/docker/dev/nginx/config/nginx.conf
new file mode 100644
index 0000000..5e0f548
--- /dev/null
+++ b/skeleton/docker/dev/nginx/config/nginx.conf
@@ -0,0 +1,40 @@
+user nginx;
+worker_processes auto;
+pid /var/run/nginx.pid;
+
+events {
+ worker_connections 1024;
+}
+
+http {
+ server_tokens off;
+
+ sendfile on;
+ tcp_nopush on;
+ tcp_nodelay on;
+
+ keepalive_requests 100;
+ keepalive_timeout 65;
+
+ types_hash_max_size 2048;
+
+ server_names_hash_bucket_size 128;
+
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ map $http_x_request_id $request_uid {
+ default $http_x_request_id;
+ "" $request_id;
+ }
+
+ log_format docker '$remote_addr - [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for" $request_time $request_uid';
+
+ access_log /var/log/nginx/access.log docker;
+ error_log /var/log/nginx/error.log error;
+
+ gzip on;
+ gzip_disable "msie6";
+
+ include /etc/nginx/conf.d/*.conf;
+}
diff --git a/skeleton/docker/dev/nginx/config/vhost.conf b/skeleton/docker/dev/nginx/config/vhost.conf
new file mode 100644
index 0000000..4119c57
--- /dev/null
+++ b/skeleton/docker/dev/nginx/config/vhost.conf
@@ -0,0 +1,34 @@
+server {
+ root /var/www/html/public;
+
+ add_header X-Request-Id $request_uid;
+
+ location / {
+ try_files $uri /index.php$is_args$args;
+ }
+
+ location ~ ^/index.php(/|$) {
+ resolver_timeout 5s;
+ resolver ${DNS_RESOLVER} valid=10s;
+
+ fastcgi_pass ${PHP_FPM_UPSTREAM};
+ fastcgi_split_path_info ^(.+\.php)(/.*)$;
+ include fastcgi_params;
+ fastcgi_param HTTP_X_REQUEST_ID $request_uid;
+ fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+ internal;
+ }
+
+ location ~ \.php$ {
+ return 404;
+ }
+
+ location /health {
+ return 200;
+ }
+
+ location /status {
+ stub_status on;
+ access_log off;
+ }
+}
diff --git a/skeleton/docker/dev/php/Dockerfile b/skeleton/docker/dev/php/Dockerfile
new file mode 100644
index 0000000..d259392
--- /dev/null
+++ b/skeleton/docker/dev/php/Dockerfile
@@ -0,0 +1,60 @@
+FROM php:7.4-fpm-alpine
+
+# php-fpm config
+ENV PHP_FPM_PM_LOG_LEVEL=warning \
+ PHP_FPM_PM_MAX_CHILDREN=5 \
+ PHP_FPM_PM_START_SERVER=2 \
+ PHP_FPM_PM_MIN_SPARE_SERVER=1 \
+ PHP_FPM_PM_MAX_SPARE_SERVER=3 \
+ PHP_FPM_PM_STATUS_PATH=/status \
+ PHP_FPM_PM_PING_PATH=/ping \
+ TIMEZONE=UTC
+
+# Install requirement
+RUN set -xe; \
+ apk add --update --no-cache \
+ bash \
+ bash-completion \
+ curl \
+ openssl \
+ git \
+ make \
+ ;
+
+# Install php extension
+RUN docker-php-ext-install -j$(nproc) \
+ opcache \
+ pdo \
+ pdo_mysql \
+ ;
+
+# Install composer
+RUN set -xe; \
+ curl -sl https://getcomposer.org/composer.phar -o /usr/local/bin/composer; \
+ chmod +x /usr/local/bin/composer;
+
+ARG DOCKER_UID
+
+RUN set -xe; \
+ apk add --no-cache --virtual .build-deps shadow; \
+ mkdir -p /var/www/html; \
+ usermod -u ${DOCKER_UID} www-data -d /var/www; \
+ chown -R www-data:www-data /var/www; \
+ apk del --no-network .build-deps;
+
+# Add bashrc
+COPY config/.bashrc /var/www/.bashrc
+
+# Configure php-fpm
+COPY config/php-fpm/* /usr/local/etc/php-fpm.d/
+
+# Configure php
+COPY config/php/* /usr/local/etc/php/conf.d/
+
+USER www-data
+
+RUN composer global require hirak/prestissimo;
+
+WORKDIR /var/www/html
+
+CMD ["/bin/sh", "-c", "bin/console cache:warmup --env=dev && php-fpm"]
diff --git a/skeleton/docker/dev/php/config/.bashrc b/skeleton/docker/dev/php/config/.bashrc
new file mode 100644
index 0000000..380a213
--- /dev/null
+++ b/skeleton/docker/dev/php/config/.bashrc
@@ -0,0 +1,72 @@
+# PROMPT COLOURS
+BLACK='\[\e[0;30m\]' # Black - Regular
+RED='\[\e[0;31m\]' # Red - Regular
+GREEN='\[\e[0;32m\]' # Green - Regular
+YELLOW='\[\e[0;33m\]' # Yellow - Regular
+BLUE='\[\e[0;34m\]' # Blue - Regular
+PURPLE='\[\e[0;35m\]' # Purple - Regular
+CYAN='\[\e[0;36m\]' # Cyan - Regular
+WHITE='\[\e[0;37m\]' # White - Regular
+BLACK_BOLD='\[\e[1;30m\]' # Black - Bold
+RED_BOLD='\[\e[1;31m\]' # Red - Bold
+GREEN_BOLD='\[\e[1;32m\]' # Green - Bold
+YELLOW_BOLD='\[\e[1;33m\]' # Yellow - Bold
+BLUE_BOLD='\[\e[1;34m\]' # Blue - Bold
+PURPLE_BOLD='\[\e[1;35m\]' # Purple - Bold
+CYAN_BOLD='\[\e[1;36m\]' # Cyan - Bold
+WHITE_BOLD='\[\e[1;37m\]' # White - Bold
+BLACK_FOREGROUND='\[\e[100m\]' # Black - Foreground
+RED_FOREGROUND='\[\e[101m\]' # Red - Foreground
+GREEN_FOREGROUND='\[\e[102m\]' # Green - Foreground
+YELLOW_FOREGROUND='\[\e[103m\]' # Yellow - Foreground
+BLUE_FOREGROUND='\[\e[104m\]' # Blue - Foreground
+PURPLE_FOREGROUND='\[\e[105m\]' # Purple - Foreground
+CYAN_FOREGROUND='\[\e[106m\]' # Cyan - Foreground
+WHITE_FOREGROUND='\[\e[107m\]' # White - Foreground
+RESET='\[\e[0m\]' # Text Reset
+
+alias ls='ls --color=auto'
+alias ll='ls -lah'
+alias grep='grep --color=auto'
+
+if [[ -f /etc/bash_completion ]]; then
+ . /etc/bash_completion
+fi
+
+if [[ -f /etc/profile.d/bash_completion.sh ]]; then
+ source /etc/profile.d/bash_completion.sh
+fi
+
+get_user() {
+ if [[ $(id -u) -eq 0 ]]; then
+ echo -n "${YELLOW_BOLD}\u"
+ else
+ echo -n "${RED_BOLD}\u"
+ fi
+}
+
+get_host() {
+ echo -n "${BLUE_BOLD}\h"
+}
+
+get_working_directory() {
+ echo -n "${PURPLE_BOLD}\w"
+}
+
+get_branch() {
+ local branch=$(git symbolic-ref HEAD --short 2> /dev/null)
+
+ if [[ ! -z ${branch} ]]; then
+ echo -n "(${branch})"
+ fi
+}
+
+get_tag() {
+ local tag=$(git describe --exact-match --tags HEAD 2> /dev/null)
+
+ if [[ ! -z ${tag} ]]; then
+ echo -n "(${tag})"
+ fi
+}
+
+export PS1="$(get_user)${CYAN_BOLD}@$(get_host) $(get_working_directory) ${YELLOW_BOLD}\$(get_branch)${GREEN_BOLD}\$(get_tag) ${BLACK_BOLD}\n\$${RESET} "
diff --git a/skeleton/docker/dev/php/config/php-fpm/fpm.conf b/skeleton/docker/dev/php/config/php-fpm/fpm.conf
new file mode 100644
index 0000000..a98ea91
--- /dev/null
+++ b/skeleton/docker/dev/php/config/php-fpm/fpm.conf
@@ -0,0 +1,4 @@
+[global]
+daemonize = no
+log_level = ${PHP_FPM_PM_LOG_LEVEL}
+error_log = /proc/self/fd/2;
diff --git a/skeleton/docker/dev/php/config/php-fpm/www.conf b/skeleton/docker/dev/php/config/php-fpm/www.conf
new file mode 100644
index 0000000..bf559cb
--- /dev/null
+++ b/skeleton/docker/dev/php/config/php-fpm/www.conf
@@ -0,0 +1,19 @@
+[www]
+pm = dynamic
+pm.max_children = ${PHP_FPM_PM_MAX_CHILDREN}
+pm.start_servers = ${PHP_FPM_PM_START_SERVER}
+pm.min_spare_servers = ${PHP_FPM_PM_MIN_SPARE_SERVER}
+pm.max_spare_servers = ${PHP_FPM_PM_MAX_SPARE_SERVER}
+
+pm.status_path = ${PHP_FPM_PM_STATUS_PATH}
+ping.path = ${PHP_FPM_PM_PING_PATH}
+
+listen = [::]:9000
+clear_env = no
+catch_workers_output = yes
+
+php_flag[log_errors] = yes
+php_flag[display_errors] = off
+
+; access.format = %{HTTP_X_REQUEST_ID}e
+access.log = /proc/self/fd/2
diff --git a/skeleton/docker/dev/php/config/php/preload.ini b/skeleton/docker/dev/php/config/php/preload.ini
new file mode 100644
index 0000000..9a12ef1
--- /dev/null
+++ b/skeleton/docker/dev/php/config/php/preload.ini
@@ -0,0 +1 @@
+opcache.preload=/var/www/html/var/cache/dev/App_KernelDevDebugContainer.preload.php
diff --git a/skeleton/docker/dev/php/config/php/timezone.ini b/skeleton/docker/dev/php/config/php/timezone.ini
new file mode 100644
index 0000000..920e4a0
--- /dev/null
+++ b/skeleton/docker/dev/php/config/php/timezone.ini
@@ -0,0 +1,2 @@
+[Date]
+date.timezone = ${TIMEZONE}
diff --git a/skeleton/docs/DOCKER.md b/skeleton/docs/DOCKER.md
new file mode 100644
index 0000000..d0552f2
--- /dev/null
+++ b/skeleton/docs/DOCKER.md
@@ -0,0 +1,38 @@
+# Docker
+
+## Requirements
+
+- [Docker](https://docs.docker.com/install/#supported-platforms) >= 18.06.0
+- [Docker compose](https://docs.docker.com/compose/install) >= 1.25
+- [Docker Proxy](https://github.com/philippe-vandermoere/docker-proxy)
+
+## Start
+
+To run the Docker's stack, execute:
+
+```bash
+make start
+```
+
+This command does:
+- Build Docker's image
+- Install PHP vendor
+- Install or upgrade database
+
+Your application is reachable at `https://skeleton_url`
+
+## Shell
+
+To connect to the PHP shell in the docker's container, execute:
+
+```bash
+make shell
+```
+
+## Logs
+
+If you want to see the logs of docker's container, execute:
+
+```bash
+make logs
+```
diff --git a/skeleton/phpcs.xml.dist b/skeleton/phpcs.xml.dist
new file mode 100644
index 0000000..c42a038
--- /dev/null
+++ b/skeleton/phpcs.xml.dist
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+ src
+ tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/skeleton/phpstan.neon b/skeleton/phpstan.neon
new file mode 100644
index 0000000..80741ce
--- /dev/null
+++ b/skeleton/phpstan.neon
@@ -0,0 +1,17 @@
+parameters:
+ level: max
+ paths:
+ - src/
+ - tests/
+
+ excludes_analyse:
+ - src/Migrations/
+
+ symfony:
+ container_xml_path: var/cache/test/App_KernelTestDebugContainer.xml
+
+ checkGenericClassInNonGenericObjectType: false
+includes:
+ - vendor/phpstan/phpstan-symfony/extension.neon
+ - vendor/phpstan/phpstan-doctrine/extension.neon
+ - vendor/phpstan/phpstan-phpunit/extension.neon
diff --git a/skeleton/phpunit.xml.dist b/skeleton/phpunit.xml.dist
new file mode 100644
index 0000000..233b36c
--- /dev/null
+++ b/skeleton/phpunit.xml.dist
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tests
+
+
+
+
+
+ src
+
+ src/Kernel.php
+
+
+
+
+
+
+
+
diff --git a/skeleton/src/Kernel.php b/skeleton/src/Kernel.php
new file mode 100644
index 0000000..3fc35f3
--- /dev/null
+++ b/skeleton/src/Kernel.php
@@ -0,0 +1,56 @@
+getProjectDir() . '/config/bundles.php';
+ foreach ($contents as $class => $envs) {
+ if ($envs[$this->environment] ?? $envs['all'] ?? false) {
+ yield new $class();
+ }
+ }
+ }
+
+ public function getProjectDir(): string
+ {
+ return \dirname(__DIR__);
+ }
+
+ protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
+ {
+ $container->addResource(new FileResource($this->getProjectDir() . '/config/bundles.php'));
+ $container->setParameter('container.dumper.inline_class_loader', true);
+ $container->setParameter('container.dumper.inline_factories', true);
+
+ $confDir = $this->getProjectDir() . '/config';
+
+ $loader->load($confDir . '/{packages}/*' . static::CONFIG_EXTS, 'glob');
+ $loader->load($confDir . '/{packages}/' . $this->environment . '/*' . static::CONFIG_EXTS, 'glob');
+ $loader->load($confDir . '/{services}/' . $this->environment . '/**/*' . static::CONFIG_EXTS, 'glob');
+ $loader->load($confDir . '/{services}/*' . static::CONFIG_EXTS, 'glob');
+ }
+
+ protected function configureRoutes(RouteCollectionBuilder $routes): void
+ {
+ $confDir = $this->getProjectDir() . '/config';
+
+ $routes->import($confDir . '/{routes}/' . $this->environment . '/*' . static::CONFIG_EXTS, '/', 'glob');
+ $routes->import($confDir . '/{routes}/*' . static::CONFIG_EXTS, '/', 'glob');
+ }
+}