Skip to content

Commit 6904d80

Browse files
committed
Initial commit.
0 parents  commit 6904d80

34 files changed

+7367
-0
lines changed

.editorconfig

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# EditorConfig is awesome: https://EditorConfig.org
2+
root = true
3+
4+
[*]
5+
charset = utf-8
6+
end_of_line = lf
7+
indent_size = 4
8+
indent_style = space
9+
insert_final_newline = true
10+
max_line_length = 120
11+
tab_width = 4
12+
trim_trailing_whitespace = true
13+
14+
[{Dockerfile,Dockerfile.*}]
15+
indent_size = 2
16+
17+
[.github/**.yaml]
18+
indent_size = 2
19+
20+
[*.md]
21+
trim_trailing_whitespace = false

.github/workflows/tests.yaml

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: PHPUnit Tests
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
phpunit:
15+
name: PHPUnit
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 10
18+
steps:
19+
# Checkout the code
20+
- name: Checkout Code
21+
uses: actions/[email protected]
22+
23+
# Setup PHP
24+
- name: Setup PHP
25+
uses: shivammathur/[email protected]
26+
with:
27+
php-version: '8.1'
28+
coverage: xdebug
29+
30+
# Install dependencies
31+
- name: Cache Composer dependencies
32+
uses: actions/[email protected]
33+
with:
34+
path: vendor
35+
key: ${{ runner.os }}-vendor-${{ hashFiles('**/composer.lock') }}
36+
- name: Install dependencies
37+
run: |
38+
composer install --prefer-dist --no-scripts --ignore-platform-reqs
39+
40+
# Run tests
41+
- name: Run Unit Tests
42+
run: |
43+
vendor/bin/phpunit --coverage-text --coverage-clover reports/coverage.xml
44+
45+
# SonarCloud
46+
# - name: SonarCloud Scan
47+
# uses: SonarSource/sonarcloud-github-action@master
48+
# env:
49+
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50+
# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/.phpunit.cache/
2+
/vendor/
3+
.php-cs-fixer.cache

.php-cs-fixer.php

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
$finder = PhpCsFixer\Finder::create()
4+
->exclude(['examples', 'vendor'])
5+
->in(__DIR__)
6+
;
7+
8+
$config = new PhpCsFixer\Config();
9+
return $config
10+
->setRules([
11+
'@PSR12' => true,
12+
'strict_param' => true,
13+
'array_syntax' => ['syntax' => 'short'],
14+
])
15+
->setRiskyAllowed(true)
16+
->setFinder($finder);

CODEOWNERS

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# global owners
2+
* @b00gizm

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 Geek Cell
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# container-facade
2+
3+
A standalone PHP library inspired by Laravel's Facade implementation, which can be used with any [PSR-11 compatible](https://www.php-fig.org/psr/psr-11/) dependency injection container (DIC) such as (the ones used by) [Symfony](https://symfony.com/), [Pimple](https://github.com/silexphp/Pimple), or [Slim](https://www.slimframework.com/).
4+
5+
## Installation
6+
7+
To use this package, require it with Composer.
8+
9+
```bash
10+
composer install geekcell/container-facade
11+
```
12+
13+
## Motivation
14+
15+
Although rare, there are situations when you want to obtain a container service without dependency injection. An example would be the [`AggregateRoot` pattern](https://martinfowler.com/bliki/DDD_Aggregate.html), which allows [dispatching domain events](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation) directly from the aggregate, which is usually created directly and not via a DIC. In such a case, a corresponding (static) service facade can provide a comparable convenience as a singleton, but without the inherent [disadvantages of the singleton pattern](https://stackoverflow.com/a/138012).
16+
17+
## Usage
18+
19+
Let's imagine you have a `Logger` service inside your DIC of choice that logs a message into a file.
20+
21+
```php
22+
<?php
23+
24+
namespace App\Service;
25+
26+
// ...
27+
28+
class Logger
29+
{
30+
public function __construct(
31+
private readonly FileWriter $writer,
32+
) {
33+
}
34+
35+
public function log(string $message, LogLevel $level = LogLevel::INFO): void
36+
{
37+
$line = sprintf(
38+
'%s (%s): %s',
39+
(new \DateTime)->format('c'),
40+
$level->value,
41+
$message,
42+
);
43+
44+
$this->writer->writeLine($line);
45+
}
46+
}
47+
```
48+
49+
If you want to "facade" this service, just create a class which extends `GeekCell\Facade\Facade`.
50+
51+
```php
52+
<?php
53+
54+
namespace App\Support\Facade;
55+
56+
use App\Service\Logger as LoggerRoot;
57+
use GeekCell\Facade\Facade;
58+
59+
class Logger extends Facade
60+
{
61+
protected static function getFacadeAccessor(): string
62+
{
63+
return 'app.logger';
64+
}
65+
}
66+
```
67+
68+
You'll have to implement the `getFacadeAccessor()` method, which returns the identifier for service inside your DIC.
69+
70+
Additionally, you have to "introduce" your DIC to the Facade. How to do this really depends in the framework you're using. In Symfony, a good opportunity to do so is to override the `boot()` method within `src/Kernel.php`.
71+
72+
```php
73+
<?php
74+
75+
namespace App;
76+
77+
use GeekCell\Facade\Facade;
78+
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
79+
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
80+
81+
class Kernel extends BaseKernel
82+
{
83+
use MicroKernelTrait;
84+
85+
public function boot()
86+
{
87+
parent::boot();
88+
89+
// This is where the magic happens!
90+
Facade::setContainer($this->container);
91+
}
92+
}
93+
```
94+
95+
To use the facade within any part of your application, just call the service as you would a static method. Behind the scenes, the call is delegated to the actual container service via `__callStatic`.
96+
97+
```php
98+
<?php
99+
100+
// ...
101+
102+
use App\Support\Facade\Logger;
103+
104+
class SomeClass
105+
{
106+
public function doStuff()
107+
{
108+
Logger::log('Calling ' __CLASS__ . '::doStuff()', LogLevel::DEBUG);
109+
110+
// The acutal method logic ...
111+
}
112+
}
113+
```
114+
115+
### Testing
116+
117+
Although the above looks like an anti pattern, it's actually very testing friendly. During unit testing, you can use the `swapMock()` method to literally swap the real service with a Mockery mock.
118+
119+
```php
120+
<?php
121+
122+
// ...
123+
124+
use App\Support\Facade\Logger;
125+
use PHPUnit\Framework\TestCase;
126+
127+
class SomeClassTest extends TestCase
128+
{
129+
public function tearDown(): void
130+
{
131+
Logger::clear();
132+
}
133+
134+
// ...
135+
136+
public function testDoStuff(): void
137+
{
138+
// Swap real service with mock
139+
$loggerMock = Logger::swapMock();
140+
141+
// Set expectations for mock
142+
$loggerMock->shouldReceive('log')->once();
143+
144+
$out = new SomeClass();
145+
$result = $out->doStuff(); // This will now call the mock!
146+
147+
// Test assertions ...
148+
}
149+
}
150+
```
151+
152+
**Hint:** You must call the `clear()` method to clear out the internally cached mock instance. For PHPUnit, you could use the `tearDown()` method to do so.
153+
154+
## Examples
155+
156+
See the `examples` directory for various sample projects with a minimal integration of this package.
157+
158+
| Framwork | Sample project |
159+
| ----------- | ------------------------------------ |
160+
| Symfony | [examples/symfony](examples/symfony) |

composer.json

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "geekcell/container-facade",
3+
"description": "A simple library for creating static facades for PSR-11 compatible container services.",
4+
"type": "library",
5+
"keywords": [
6+
"container",
7+
"dic",
8+
"facade",
9+
"php-fig",
10+
"psr-11"
11+
],
12+
"license": "MIT",
13+
"version": "1.0.0",
14+
"authors": [
15+
{
16+
"name": "Pascal Cremer",
17+
"email": "[email protected]"
18+
}
19+
],
20+
"require": {
21+
"psr/container": "^2.0"
22+
},
23+
"require-dev": {
24+
"phpstan/phpstan": "^1.9",
25+
"friendsofphp/php-cs-fixer": "^3.14",
26+
"mockery/mockery": "^1.5",
27+
"phpstan/phpstan-mockery": "^1.1",
28+
"phpunit/phpunit": "^10.0"
29+
},
30+
"autoload": {
31+
"psr-4": {
32+
"GeekCell\\Facade\\": "src/",
33+
"GeekCell\\Facade\\Test\\": "tests/"
34+
}
35+
},
36+
"scripts": {
37+
"gc:tests": "phpunit --testdox --colors=always",
38+
"gc:cs-lint": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix --config .php-cs-fixer.php --diff -vvv --dry-run",
39+
"gc:cs-fix": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix --config .php-cs-fixer.php -vvv || true",
40+
"gc:phpstan": "phpstan analyse --memory-limit=2G --no-progress --level=8"
41+
}
42+
}

0 commit comments

Comments
 (0)