From c4e04a1cdfa1674ace611300515bd9fdedb3d99f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Faugeron?= Date: Sat, 31 Jan 2026 11:04:14 +0000 Subject: [PATCH 01/10] chore: update LICENSE to 2026 --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index e6896e8..395022b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015-2024 Loïc Faugeron +Copyright (c) 2015-2026 Loïc Faugeron Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From c93e2389deae1cd76d786b592c4279aa2eb6d1f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Faugeron?= Date: Sat, 31 Jan 2026 11:05:48 +0000 Subject: [PATCH 02/10] chore: setup git push hook to check v prefix on tags --- bin/hooks/pre-push | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100755 bin/hooks/pre-push diff --git a/bin/hooks/pre-push b/bin/hooks/pre-push new file mode 100755 index 0000000..96d77f9 --- /dev/null +++ b/bin/hooks/pre-push @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +# Pre-push hook to enforce "v" prefix on tags +# This prevents pushing tags that don't start with "v" + +while read local_ref local_sha remote_ref remote_sha +do + # Skip deletions + if [[ $local_sha == "0000000000000000000000000000000000000000" ]]; then + continue + fi + + # Check if this is a tag being pushed + if [[ $local_ref =~ ^refs/tags/ ]]; then + tag_name="${local_ref#refs/tags/}" + + # Check if tag starts with "v" + if [[ ! $tag_name =~ ^v ]]; then + echo "Error: Tag '$tag_name' does not start with 'v' prefix" + echo "Please create tags with 'v' prefix (e.g., v1.0.0, v3.0.2)" + echo "" + echo "To fix this:" + echo " 1. Delete the tag: git tag -d $tag_name" + echo " 2. Create it with 'v' prefix: git tag v$tag_name" + echo " 3. Push again: git push --tags" + exit 1 + fi + + echo "[OK] Tag '$tag_name' has correct 'v' prefix" + fi +done + +exit 0 From 1568a5200871f416778a4d7ba41b88a82445358a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Faugeron?= Date: Sat, 31 Jan 2026 11:06:55 +0000 Subject: [PATCH 03/10] chore: dockerized for local development --- Dockerfile | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ef4f3c9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +# syntax=docker/dockerfile:1 + +### +# PHP Dev Container +# Utility Tools: PHP, bash, Composer +### +FROM php:8.0-cli AS php_dev_container + +# Composer environment variables: +# * default user is superuser (root), so allow them +# * put cache directory in a readable/writable location +# _Note_: When running `composer` in container, use `--no-cache` option +ENV COMPOSER_ALLOW_SUPERUSER=1 \ + COMPOSER_CACHE_DIR=/tmp/.composer/cache + +# Update apt sources to use archived Debian repositories +RUN sed -i 's/deb.debian.org/archive.debian.org/g' /etc/apt/sources.list \ + && sed -i '/debian-security/d' /etc/apt/sources.list \ + && sed -i '/stretch-updates/d' /etc/apt/sources.list + +# Install dependencies: +# * git: for composer to download packages from source +# * libzip-dev: for composer packages that use ZIP archives +# * unzip: for composer to extract packages +# _Note (Hadolint)_: No version locking for dev container +# hadolint ignore=DL3008 +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + libzip-dev \ + unzip \ + && rm -rf /var/lib/apt/lists/* + +# Copy Composer binary from composer image +# _Note (Hadolint)_: False positive as `COPY` works with images too +# See: https://github.com/hadolint/hadolint/issues/197#issuecomment-1016595425 +# hadolint ignore=DL3022 +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +WORKDIR /app From d5135aa935a61f2132526d8cdb6f341868bb1bb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Faugeron?= Date: Sat, 31 Jan 2026 11:11:50 +0000 Subject: [PATCH 04/10] chore: upgraded PHP CS fixer to v2.19.3 --- .php-cs-fixer.dist.php | 47 ++++++++++++++++++++++++++++++++++++++++++ .php_cs | 18 ---------------- composer.json | 2 +- 3 files changed, 48 insertions(+), 19 deletions(-) create mode 100644 .php-cs-fixer.dist.php delete mode 100644 .php_cs diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..3aa6af7 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,47 @@ + php-cs-fixer describe blank_line_before_statement + * ``` + * + * ------------------------------------------------------------------------------ + * + * `new \PhpCsFixer\Finder()` is equivalent to: + * + * ```php + * \Symfony\Component\Finder\Finder::create() + * ->files() + * ->name('/\.php$/') + * ->exclude('vendor') + * ->ignoreVCSIgnored(true) // Follow rules establish in .gitignore + * ->ignoreDotFiles(false) // Do not ignore files starting with `.`, like `.php-cs-fixer-dist.php` + * ; + * ``` + */ + +$finder = (new PhpCsFixer\Finder()) + ->in(__DIR__) +; + +return (new PhpCsFixer\Config()) + ->setRules([ + '@Symfony' => true, + + // [Symfony] defaults to `camelCase`, we set it to `snake_case` (phpspec style) + 'php_unit_method_casing' => ['case' => 'snake_case'], + + // [Symfony] defaults to `true`, we set it to `false` for phpspec + 'visibility_required' => false, + ]) + ->setUsingCache(true) + ->setFinder($finder) +; diff --git a/.php_cs b/.php_cs deleted file mode 100644 index e5dca0d..0000000 --- a/.php_cs +++ /dev/null @@ -1,18 +0,0 @@ -exclude('vendor') - ->in(__DIR__) -; - -return PhpCsFixer\Config::create() - ->setRules([ - '@Symfony' => true, - 'visibility_required' => false, - 'array_syntax' => [ - 'syntax' => 'short', - ], - ]) - ->setUsingCache(true) - ->setFinder($finder) -; diff --git a/composer.json b/composer.json index f66c8bc..d98a7e8 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "php": "^7.2 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.16", + "friendsofphp/php-cs-fixer": "^2.19.3", "phpspec/phpspec": "^6.1 || ^7.0" } } From c5087dfa2307b8686b70c274128c911914089beb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Faugeron?= Date: Sat, 31 Jan 2026 11:15:01 +0000 Subject: [PATCH 05/10] chore: installed rector as a dev dependency --- composer.json | 3 ++- rector.php | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 rector.php diff --git a/composer.json b/composer.json index d98a7e8..c4fa6af 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "^2.19.3", - "phpspec/phpspec": "^6.1 || ^7.0" + "phpspec/phpspec": "^6.1 || ^7.0", + "rector/rector": "^1.2.10" } } diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..f2db8c0 --- /dev/null +++ b/rector.php @@ -0,0 +1,29 @@ +withCache( + '/tmp/rector', + FileCacheStorage::class + ) + ->withPaths([ + __DIR__, + ]) + ->withSkip([ + // —— Excluded paths ——————————————————————————————————————————————————— + // Excluded folders + __DIR__.'/vendor', + + // —— Excluded rules ——————————————————————————————————————————————————— + ]) + ->withSets([ + // —— PHP —————————————————————————————————————————————————————————————— + SetList::PHP_72, + ]) + ->withRules([ + ]); From 2eceb1fd1e49cf7fe7f369707621107bb737faa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Faugeron?= Date: Sat, 31 Jan 2026 11:16:08 +0000 Subject: [PATCH 06/10] chore: installed swiss-knife as a dev dependency --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c4fa6af..f4e0958 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^2.19.3", "phpspec/phpspec": "^6.1 || ^7.0", - "rector/rector": "^1.2.10" + "rector/rector": "^1.2.10", + "rector/swiss-knife": "^2.3" } } From 3f00bb84761f0c8fe890dfbc493c240a10c8450a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Faugeron?= Date: Sat, 31 Jan 2026 11:16:53 +0000 Subject: [PATCH 07/10] chore: installed phpstan as a dev dependency --- composer.json | 1 + phpstan-baseline.neon | 2 ++ phpstan.neon.dist | 8 ++++++++ 3 files changed, 11 insertions(+) create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon.dist diff --git a/composer.json b/composer.json index f4e0958..5965ac5 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^2.19.3", "phpspec/phpspec": "^6.1 || ^7.0", + "phpstan/phpstan": "1.12.32", "rector/rector": "^1.2.10", "rector/swiss-knife": "^2.3" } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..08872a3 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,2 @@ +parameters: + ignoreErrors: [] diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..7319f18 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,8 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 0 + phpVersion: 70200 + paths: + - src/ From 9349eefb250a50ba55e5f2cb5329d64975d188c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Faugeron?= Date: Sat, 31 Jan 2026 11:18:20 +0000 Subject: [PATCH 08/10] chore: changed tooling from scripts to Makefile --- Makefile | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..66b716e --- /dev/null +++ b/Makefile @@ -0,0 +1,110 @@ +# Parameters (optional) +# * `arg`: arbitrary arguments to pass to rules (default: none) +arg ?= + +# Docker containers +LIB_SERVICE = memio-model + +# Executables +COMPOSER = docker exec $(LIB_SERVICE) composer +PHP_CS_FIXER = docker exec $(LIB_SERVICE) php vendor/bin/php-cs-fixer +PHPSPEC = docker exec $(LIB_SERVICE) php vendor/bin/phpspec +PHPSTAN = docker exec $(LIB_SERVICE) php vendor/bin/phpstan --memory-limit=256M +RECTOR = docker exec $(LIB_SERVICE) php vendor/bin/rector +SWISS_KNIFE = docker exec $(LIB_SERVICE) php vendor/bin/swiss-knife + +# Misc +.DEFAULT_GOAL = help +.PHONY: * + +## —— 🔭 The Super Secret Makefile —————————————————————————————————————————————— +## Based on https://github.com/dunglas/symfony-docker +## (arg) denotes the possibility to pass "arg=" parameter to the target +## this allows to add command and options, example: make composer arg='dump --optimize' +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 🐳 ———————————————————————————————————————————————————————————————— +docker: ## Runs Docker (arg, eg `arg='ps'`) + @docker $(arg) + +docker-init: ## Builds the Docker image and starts the container in detached mode + @docker build --platform linux/amd64 --pull -t $(LIB_SERVICE) . + @docker run -d --platform linux/amd64 --name $(LIB_SERVICE) -v .:/app $(LIB_SERVICE) tail -f /dev/null + +docker-down: ## Stops and removes the container + @docker rm -f $(LIB_SERVICE) 2>/dev/null || true + +docker-bash: ## Opens a (bash) shell in the container + @docker exec -it $(LIB_SERVICE) bash + +## —— PHP 🐘 ——————————————————————————————————————————————————————————————————— +composer: ## Runs Composer (arg, eg `arg='outdated'`) + @$(COMPOSER) $(arg) + +composer-install: ## Install dependencies (arg, eg `arg='--no-dev'`) + @$(COMPOSER) install --optimize-autoloader $(arg) + +composer-update: ## Updates dependencies (arg, eg `arg='--no-dev'`) + @$(COMPOSER) update --optimize-autoloader $(arg) + +composer-dump: ## Dumps autoloader (arg, eg `arg='--classmap-authoritative'`) + @$(COMPOSER) dump-autoload --optimize --strict-psr --strict-ambiguous $(arg) + +cs-check: ## Checks CS with PHP-CS-Fixer (arg, eg `arg='../monolith/web'`) + @$(PHP_CS_FIXER) fix --dry-run --verbose $(arg) + +cs-fix: ## Fixes CS with Swiss Knife and PHP-CS-Fixer + @$(SWISS_KNIFE) namespace-to-psr-4 spec/Memio/Model --namespace-root 'spec\\Memio\\Model\\' + @$(SWISS_KNIFE) namespace-to-psr-4 src/Memio/Model --namespace-root 'Memio\\Model\\' + @$(PHP_CS_FIXER) fix --verbose $(arg) + +phpstan: ## Static Analysis with PHPStan (arg, eg `arg='--level=6'`) + @$(PHPSTAN) analyze $(arg) + +swiss-knife: ## Automated refactorings with Swiss Knife (arg, eg `arg='namespace-to-psr-4 src --namespace-root \'App\\\''`) + @$(SWISS_KNIFE) $(arg) + +phpspec: ## Runs the specifications with phpspec (arg, eg `arg='--format=dot'`) + @$(PHPSPEC) --no-interaction run $(arg) + +rector-fix: ## Automated refactorings with Rector (arg, eg `arg='--clear-cache'`) + @$(RECTOR) $(arg) + +rector-check: ## Refactoring checks with Rector + @$(RECTOR) process --dry-run + +## —— Lib 📚 ——————————————————————————————————————————————————————————————————— +lib-init: ## First install / resetting (Docker build, up, etc) + @echo '' + @echo ' // Installing git hooks...' + @git config core.hooksPath bin/hooks + @echo '' + @echo ' // Stopping lib docker services...' + @$(MAKE) docker-down + @echo '' + @echo ' // Starting lib docker services...' + @$(MAKE) docker-init + @echo '' + @echo ' [OK] Lib initialized' + +lib-qa: ## Runs full QA pipeline (composer-dump, cs-check, phpstan, rector-check, phpspec) + @echo '' + @echo ' // Running composer dump...' + @$(MAKE) composer-dump + @echo '' + @echo ' // Running PHP CS Fixer...' + @$(MAKE) cs-check + @echo '' + @echo ' // Running PHPStan...' + @$(MAKE) phpstan + @echo '' + @echo ' // Running Rector...' + @$(MAKE) rector-check + @echo '' + @echo ' // Running phpspec...' + @$(MAKE) phpspec + @echo '' + @echo ' [OK] QA done' From cbdc5b797ef543e93c4d7cac135fc051c85fa901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Faugeron?= Date: Sat, 31 Jan 2026 11:20:29 +0000 Subject: [PATCH 09/10] chore: setup Github Actions --- .github/workflows/ci.yml | 53 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fc74344 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + qa: + name: QA (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + + strategy: + matrix: + php: ['8.0'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring + coverage: none + + - name: Get Composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress --optimize-autoloader + + - name: Check coding standards + run: vendor/bin/php-cs-fixer fix --dry-run --verbose + + - name: Run PHPStan + run: vendor/bin/phpstan analyze --memory-limit=256M + + - name: Run Rector + run: vendor/bin/rector process --dry-run + + - name: Run phpspec + run: vendor/bin/phpspec --no-interaction run From bba9744b79a9c041ee1e65cb8a8eb73acdf6aae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Faugeron?= Date: Sat, 31 Jan 2026 11:21:17 +0000 Subject: [PATCH 10/10] chore: updated documentation --- CONTRIBUTING.md | 36 ++++++++++++++++++++++++++++-------- README.md | 8 ++++++-- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f44c7b4..f400a0c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,17 +12,29 @@ changes, improvements or alternatives may be given). Here's some tips to make you the best contributor ever: +* [Getting started](#getting-started) * [Standard code](#standard-code) * [Specifications](#specifications) +* [Full QA check](#full-qa-check) * [Keeping your fork up-to-date](#keeping-your-fork-up-to-date) +## Getting started + +First, set up your local environment: + +```console +make lib-init +``` + +> **Note**: Run `make` or `make help` to see all available commands. + ## Standard code Use [PHP CS fixer](http://cs.sensiolabs.org/) to make your code compliant with Memio's coding standards: ```console -$ ./vendor/bin/php-cs-fixer fix . +make cs-fix ``` ## Specifications @@ -32,35 +44,43 @@ Memio drives its development using [phpspec](http://www.phpspec.net/). First bootstrap the code for the Specification: ```console -$ phpspec describe 'Memio\Memio\MyNewUseCase' +make phpspec arg="describe 'Memio\Model\MyNewClass'" ``` Next, write the actual code of the Specification: ```console -$ $EDITOR spec/Memio/SpecGen/MyNewUseCase.php +$EDITOR spec/Memio/Model/MyNewClassSpec.php ``` -Then bootstrap the code for the corresponding Use Case: +Then bootstrap the code for the corresponding class: ```console -$ phpspec run +make phpspec ``` -Follow that by writing the actual code of the Use Case: +Follow that by writing the actual code of the class: ```console -$ $EDITOR src/Memio/SpecGen/MyNewUseCase.php +$EDITOR src/Memio/Model/MyNewClass.php ``` Finally run the specification: ```console -$ phpspec run +make phpspec ``` Results should be green! +## Full QA check + +Before submitting your pull request, run the full QA pipeline: + +```console +make lib-qa +``` + ## Keeping your fork up-to-date To keep your fork up-to-date, you should track the upstream (original) one diff --git a/README.md b/README.md index e93d37c..0472105 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,12 @@ Have a look at [the main respository](http://github.com/memio/memio) to discover Memio uses [phpspec](http://phpspec.net/), which means the tests also provide the documentation. Not convinced? Then clone this repository and run the following commands: - composer install - ./vendor/bin/phpspec run -n -f pretty +```console +make lib-init # Set up Docker environment +make phpspec arg='--format pretty' # Run the specifications +``` + +> **Note**: Run `make` or `make help` to see all available commands. You can see the current and past versions using one of the following: