From 701e2cfd1c1b1ada772af87d2a4cb6fa80a909b4 Mon Sep 17 00:00:00 2001 From: Fabrizio Di Napoli Date: Sat, 14 Feb 2026 22:33:21 +0100 Subject: [PATCH 1/4] Improvements --- .github/workflows/php.yml | 16 +++++- README.md | 19 ++++++- composer.json | 17 ++++-- composer.lock | 57 ++++++++++++++++++- src/Structural/Facade/CoffeeMakerFacade.php | 4 +- src/Structural/Proxy/ApiProxy.php | 4 +- .../AbstractFactory/BmwCarFactoryTest.php | 5 +- .../Builder/ClassicWatchBuilderTest.php | 1 - test/Creational/Builder/ClassicWatchTest.php | 1 - test/Creational/Builder/DirectorTest.php | 1 - .../Builder/SportWatchBuilderTest.php | 1 - test/Creational/Builder/SportWatchTest.php | 1 - test/Structural/Composite/PhoneTest.php | 1 - test/Structural/Decorator/ApiResponseTest.php | 1 - .../Structural/Decorator/JsonResponseTest.php | 1 - test/Structural/Decorator/XmlResponseTest.php | 1 - .../Facade/CoffeeMakerFacadeTest.php | 1 - .../Flyweight/FlyweightFactoryTest.php | 1 - test/Structural/Proxy/ApiTest.php | 1 - 19 files changed, 109 insertions(+), 25 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 3b5e0d2..8e28c1a 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -37,7 +37,19 @@ jobs: - name: Install dependencies run: ./composer.phar install --prefer-dist --no-progress - - name: Run test suite + - name: Check pattern structure + run: ./composer.phar structure:check + + - name: Run test suite with coverage env: XDEBUG_MODE: coverage - run: ./composer.phar test + run: ./composer.phar test:coverage + + - name: Check coverage threshold + run: ./composer.phar coverage:check + + - name: Run coding standards + run: ./composer.phar cs + + - name: Run static analysis + run: ./composer.phar stan diff --git a/README.md b/README.md index 590349b..3bdc07e 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,30 @@ ## Just another collection of design patterns implementations in PHP ## Requirements -- PHP 8.3+ +- PHP 8.4.1+ ## Setup - Run `./composer.phar install` ## Run Tests - Run `./composer.phar test` +- Run `./composer.phar test:coverage` ## Run Coding Standards - Run `./composer.phar cs` +- Run `./composer.phar cs:fix` + +## Run Static Analysis +- Run `./composer.phar stan` + +## CI Pipeline (local) +- Run `./composer.phar ci` + +## Contributing +- Create feature branches from `master`. +- Keep pattern folders mirrored in `src//` and `test//`. +- Include/maintain `README.md` in each pattern folder. +- Run `./composer.phar ci` before opening a PR. + +## Project Conventions +- See `docs/PATTERN_STRUCTURE.md`. diff --git a/composer.json b/composer.json index 789fe7f..9c96326 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ ], "license": "MIT", "require": { - "php": ">=8.3" + "php": ">=8.4.1" }, "autoload": { "psr-4": { @@ -23,14 +23,23 @@ }, "require-dev": { "phpunit/phpunit": "^13.0", + "phpstan/phpstan": "^2.1", "squizlabs/php_codesniffer": "^4.0" }, "scripts": { - "test": "vendor/bin/phpunit test/", + "test": "vendor/bin/phpunit --no-coverage test/", + "test:coverage": "vendor/bin/phpunit --coverage-clover coverage/clover.xml test/", "cs": "vendor/bin/phpcs src test", + "cs:fix": "vendor/bin/phpcbf src test", + "stan": "vendor/bin/phpstan analyse --configuration=phpstan.neon --no-progress", + "structure:check": "bash scripts/check-pattern-structure.sh", + "coverage:check": "bash scripts/check-coverage.sh 70 coverage/clover.xml", "ci": [ - "@test", - "@cs" + "@structure:check", + "@test:coverage", + "@coverage:check", + "@cs", + "@stan" ] } } diff --git a/composer.lock b/composer.lock index 4637d3f..53b9085 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a62f16ccdf9745c28fc45b98872891d0", + "content-hash": "8f425deeeab3642b367510798d7a1c16", "packages": [], "packages-dev": [ { @@ -243,6 +243,59 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "phpstan/phpstan", + "version": "2.1.39", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c6f73a2af4cbcd99c931d0fb8f08548cc0fa8224", + "reference": "c6f73a2af4cbcd99c931d0fb8f08548cc0fa8224", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-02-11T14:48:56+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "13.0.1", @@ -1888,7 +1941,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.3" + "php": ">=8.4.1" }, "platform-dev": [], "plugin-api-version": "2.6.0" diff --git a/src/Structural/Facade/CoffeeMakerFacade.php b/src/Structural/Facade/CoffeeMakerFacade.php index d959d8e..4b36a12 100644 --- a/src/Structural/Facade/CoffeeMakerFacade.php +++ b/src/Structural/Facade/CoffeeMakerFacade.php @@ -24,9 +24,9 @@ public function makeCups(int $numberOfCupsToMake): array $numberOfCupsToMake = $this->checkCupsToMake($numberOfCupsToMake); $coffeeCups = []; - for ($i=1; $i<$numberOfCupsToMake+1; $i++) { + for ($i = 1; $i < $numberOfCupsToMake + 1; $i++) { $products = implode(',', $this->getProducts()); - $coffeeCups["coffee #".$i] = $products; + $coffeeCups["coffee #" . $i] = $products; } return $coffeeCups; } diff --git a/src/Structural/Proxy/ApiProxy.php b/src/Structural/Proxy/ApiProxy.php index cda0fc3..4d4b56e 100644 --- a/src/Structural/Proxy/ApiProxy.php +++ b/src/Structural/Proxy/ApiProxy.php @@ -4,7 +4,9 @@ class ApiProxy extends Api { - public function __construct(private ?Api $wrapper) {} + public function __construct(private ?Api $wrapper) + { + } public function doApiCall(string $url, array $data, string $method): array|null { diff --git a/test/Creational/AbstractFactory/BmwCarFactoryTest.php b/test/Creational/AbstractFactory/BmwCarFactoryTest.php index 3879831..b396a3b 100644 --- a/test/Creational/AbstractFactory/BmwCarFactoryTest.php +++ b/test/Creational/AbstractFactory/BmwCarFactoryTest.php @@ -63,6 +63,9 @@ public function testShouldCreateABmwFamilyCar() private function createCarDescription(string $type): string { - return "BMW $type car!" . PHP_EOL . "Name:" . (self::GENERIC_CAR_NAME) . "" . PHP_EOL . "Color:" . (self::GENERIC_CAR_COLOR) . "" . PHP_EOL . "Engine:" . (self::GENERIC_CAR_ENGINE_SPECS) . ""; + return "BMW $type car!" + . PHP_EOL . "Name:" . self::GENERIC_CAR_NAME + . PHP_EOL . "Color:" . self::GENERIC_CAR_COLOR + . PHP_EOL . "Engine:" . self::GENERIC_CAR_ENGINE_SPECS; } } diff --git a/test/Creational/Builder/ClassicWatchBuilderTest.php b/test/Creational/Builder/ClassicWatchBuilderTest.php index f9c8859..832ecf9 100644 --- a/test/Creational/Builder/ClassicWatchBuilderTest.php +++ b/test/Creational/Builder/ClassicWatchBuilderTest.php @@ -8,7 +8,6 @@ class ClassicWatchBuilderTest extends TestCase { - public function testShouldCreateAClassicWatch() { $classicWatch = (new ClassicWatchBuilder()) diff --git a/test/Creational/Builder/ClassicWatchTest.php b/test/Creational/Builder/ClassicWatchTest.php index 87e70bc..0a979ce 100644 --- a/test/Creational/Builder/ClassicWatchTest.php +++ b/test/Creational/Builder/ClassicWatchTest.php @@ -8,7 +8,6 @@ class ClassicWatchTest extends TestCase { - public function testShouldAddAComponentToAWatch() { $watch = new ClassicWatch(); diff --git a/test/Creational/Builder/DirectorTest.php b/test/Creational/Builder/DirectorTest.php index 7117126..33f735f 100644 --- a/test/Creational/Builder/DirectorTest.php +++ b/test/Creational/Builder/DirectorTest.php @@ -10,7 +10,6 @@ class DirectorTest extends TestCase { - public function testShouldCreateASportWatch() { $sportWatch = (new Director())->build(new SportWatchBuilder()); diff --git a/test/Creational/Builder/SportWatchBuilderTest.php b/test/Creational/Builder/SportWatchBuilderTest.php index 9dceffb..858ffc5 100644 --- a/test/Creational/Builder/SportWatchBuilderTest.php +++ b/test/Creational/Builder/SportWatchBuilderTest.php @@ -8,7 +8,6 @@ class SportWatchBuilderTest extends TestCase { - public function testShouldCreateASportWatch() { $watch = (new SportWatchBuilder()) diff --git a/test/Creational/Builder/SportWatchTest.php b/test/Creational/Builder/SportWatchTest.php index 1900260..31f57ba 100644 --- a/test/Creational/Builder/SportWatchTest.php +++ b/test/Creational/Builder/SportWatchTest.php @@ -8,7 +8,6 @@ class SportWatchTest extends TestCase { - public function testShouldAddAComponentToASportWatch() { $watch = new SportWatch(); diff --git a/test/Structural/Composite/PhoneTest.php b/test/Structural/Composite/PhoneTest.php index ca2796f..ad3d24a 100644 --- a/test/Structural/Composite/PhoneTest.php +++ b/test/Structural/Composite/PhoneTest.php @@ -9,7 +9,6 @@ class PhoneTest extends TestCase { - public function testShouldGetPhonePrice() { $phone = new Phone( diff --git a/test/Structural/Decorator/ApiResponseTest.php b/test/Structural/Decorator/ApiResponseTest.php index 0e28612..3083e98 100644 --- a/test/Structural/Decorator/ApiResponseTest.php +++ b/test/Structural/Decorator/ApiResponseTest.php @@ -7,7 +7,6 @@ class ApiResponseTest extends TestCase { - public function testShouldReturnARawApiResponse() { $expected = [ diff --git a/test/Structural/Decorator/JsonResponseTest.php b/test/Structural/Decorator/JsonResponseTest.php index 2896006..720c3f5 100644 --- a/test/Structural/Decorator/JsonResponseTest.php +++ b/test/Structural/Decorator/JsonResponseTest.php @@ -8,7 +8,6 @@ class JsonResponseTest extends TestCase { - public function testShouldBeAbleToConvertResponseToJsonString() { $expected = '{"message":"api response to json"}'; diff --git a/test/Structural/Decorator/XmlResponseTest.php b/test/Structural/Decorator/XmlResponseTest.php index 6c7c2a2..0adfa94 100644 --- a/test/Structural/Decorator/XmlResponseTest.php +++ b/test/Structural/Decorator/XmlResponseTest.php @@ -8,7 +8,6 @@ class XmlResponseTest extends TestCase { - public function testShouldBeAbleToConvertResponseToXml() { $expected = <<assertEquals(0, (new FlyweightFactory())->count()); diff --git a/test/Structural/Proxy/ApiTest.php b/test/Structural/Proxy/ApiTest.php index ce7473c..d3e6998 100644 --- a/test/Structural/Proxy/ApiTest.php +++ b/test/Structural/Proxy/ApiTest.php @@ -7,7 +7,6 @@ class ApiTest extends TestCase { - public function testShouldPerformAnApiCallSuccessfully() { $expects = [ From 4f938b84168142337e3ad49e60c6eecef5bc8577 Mon Sep 17 00:00:00 2001 From: Fabrizio Di Napoli Date: Sat, 14 Feb 2026 22:33:57 +0100 Subject: [PATCH 2/4] Improvements --- docs/PATTERN_STRUCTURE.md | 19 ++++++++++++++++ phpstan.neon | 5 +++++ scripts/check-coverage.sh | 35 ++++++++++++++++++++++++++++++ scripts/check-pattern-structure.sh | 26 ++++++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 docs/PATTERN_STRUCTURE.md create mode 100644 phpstan.neon create mode 100755 scripts/check-coverage.sh create mode 100755 scripts/check-pattern-structure.sh diff --git a/docs/PATTERN_STRUCTURE.md b/docs/PATTERN_STRUCTURE.md new file mode 100644 index 0000000..b37e8b6 --- /dev/null +++ b/docs/PATTERN_STRUCTURE.md @@ -0,0 +1,19 @@ +# Pattern Structure Conventions + +Each pattern should follow the same layout to keep navigation predictable. + +## Source layout + +- `src///` contains implementation classes. +- `src///README.md` explains intent and usage. + +## Test layout + +- `test///` contains tests for that pattern. +- Test classes should map to source classes when possible. + +## Categories + +- Creational +- Structural +- Behavioral diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..0e974a1 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 1 + paths: + - src + - test diff --git a/scripts/check-coverage.sh b/scripts/check-coverage.sh new file mode 100755 index 0000000..4e4e6b7 --- /dev/null +++ b/scripts/check-coverage.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +set -euo pipefail + +MIN_COVERAGE="${1:-70}" +CLOVER_FILE="${2:-coverage/clover.xml}" + +if [[ ! -f "$CLOVER_FILE" ]]; then + echo "Coverage file not found: $CLOVER_FILE" + exit 1 +fi + +METRICS_LINE="$(grep -m1 ' Date: Sat, 14 Feb 2026 22:38:48 +0100 Subject: [PATCH 3/4] Fix coverage parser for Clover metrics selection --- scripts/check-coverage.sh | 45 +++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/scripts/check-coverage.sh b/scripts/check-coverage.sh index 4e4e6b7..a2f6248 100755 --- a/scripts/check-coverage.sh +++ b/scripts/check-coverage.sh @@ -10,15 +10,42 @@ if [[ ! -f "$CLOVER_FILE" ]]; then exit 1 fi -METRICS_LINE="$(grep -m1 'xpath("//metrics[@statements and @coveredstatements]"); + if ($metrics === false || count($metrics) === 0) { + fwrite(STDERR, "No coverage metrics found in $file\n"); + exit(1); + } + + $covered = -1; + $statements = -1; + + foreach ($metrics as $node) { + $nodeStatements = (int) $node["statements"]; + $nodeCovered = (int) $node["coveredstatements"]; + + if ($nodeStatements > $statements) { + $statements = $nodeStatements; + $covered = $nodeCovered; + } + } + + if ($statements <= 0) { + fwrite(STDERR, "Invalid coverage statement count in $file\n"); + exit(1); + } + + echo $covered . " " . $statements . PHP_EOL; + ' "$CLOVER_FILE" +) if [[ -z "$STATEMENTS" || -z "$COVERED" || "$STATEMENTS" -eq 0 ]]; then echo "Invalid coverage metrics in $CLOVER_FILE" From c8241b24e00ac70e7e8b4ff57654fdcdd7d6bc46 Mon Sep 17 00:00:00 2001 From: Fabrizio Di Napoli Date: Sat, 14 Feb 2026 22:43:14 +0100 Subject: [PATCH 4/4] Improvement of GHA --- .github/workflows/php.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 8e28c1a..0858ca9 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -1,10 +1,8 @@ -name: PHP Composer - +name: ci on: - push: - branches: [ master ] pull_request: - branches: [ master ] + push: + branches: [main] jobs: build: