Skip to content

Commit fc768a3

Browse files
authored
Merge pull request #1 from MaplePHP/develop
Skeleton project updates
2 parents f94d8ab + fb8be01 commit fc768a3

24 files changed

+622
-201
lines changed

.github/workflows/php.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: PHP Unitary
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
pull_request:
7+
branches: ["main"]
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
build:
15+
runs-on: ubuntu-latest
16+
env:
17+
COMPOSER_ROOT_VERSION: 1.x-dev
18+
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- name: Cache Composer packages
23+
uses: actions/cache@v3
24+
with:
25+
path: vendor
26+
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
27+
restore-keys: |
28+
${{ runner.os }}-php-
29+
30+
- name: Install dependencies
31+
run: composer install --prefer-dist --no-progress
32+
33+
- name: Run test suite
34+
run: php vendor/bin/unitary

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,19 @@ composer require maplephp/emitron
3232
Emitron includes a robust request handler that executes PSR-15 middlewares in sequence, returning a fully PSR-7 compliant response.
3333

3434
```php
35-
use MaplePHP\Emitron\RequestHandler;use MaplePHP\Http\Environment;use MaplePHP\Http\ServerRequest;use MaplePHP\Http\Uri;
35+
use Psr\Http\Message\RequestInterface;
36+
use Psr\Http\Message\ResponseInterface;
37+
use MaplePHP\Emitron\RequestHandler;
38+
use MaplePHP\Emitron\Emitters\HttpEmitter;
39+
use MaplePHP\Http\Environment;
40+
use MaplePHP\Http\ServerRequest;
41+
use MaplePHP\Http\Uri;
42+
use MaplePHP\Emitron\Middlewares\{
43+
ContentLengthMiddleware,
44+
GzipMiddleware,
45+
HeadRequestMiddleware
46+
};
47+
use App\Controllers\MyController;
3648

3749
// Use MaplePHP HTTP library or any other PSR-7 implementation
3850
$env = new Environment();
@@ -48,8 +60,23 @@ $middlewares = [
4860
];
4961

5062
// Run the middleware stack
51-
$handler = new RequestHandler($middlewares, $factory);
63+
64+
65+
$factory = new ResponseFactory($bodyStream);
66+
// $finalHandler = new ControllerRequestHandler($factory, [MyController::class, "index"]);
67+
$finalHandler = new ControllerRequestHandler($factory, function(RequestInterface $request, ResponseInterface $response) {
68+
$response->getBody()->write("Lorem ipsum dolor sit amet, consectetur adipiscing elit.");
69+
return $response;
70+
});
71+
72+
$handler = new RequestHandler($middlewares, $finalHandler);
5273
$response = $handler->handle($request);
74+
75+
// Emit the execute headers and response correctly
76+
//$emit = new CliEmitter($response, $request);
77+
$emit = new HttpEmitter();
78+
$emit->emit($response, $request);
79+
5380
```
5481

5582
Each middleware conforms to `Psr\Http\Server\MiddlewareInterface`, allowing you to plug in your own or third-party middlewares with no additional setup.

composer.json

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
"homepage": "https://wazabii.se"
2929
}
3030
],
31+
"scripts": {
32+
"test": "php vendor/bin/unitary"
33+
},
3134
"require": {
3235
"php": ">=8.2",
3336
"psr/http-server-middleware": "^1.0",
@@ -37,15 +40,17 @@
3740
"require-dev": {
3841
"maplephp/unitary": "^2.0"
3942
},
40-
"extra": {
41-
"branch-alias": {
42-
"dev-main": "1.x-dev"
43-
}
44-
},
4543
"autoload": {
4644
"psr-4": {
4745
"MaplePHP\\Emitron\\": "src"
4846
}
4947
},
50-
"minimum-stability": "dev"
48+
"extra": {
49+
"branch-alias": {
50+
"dev-main": "1.x-dev",
51+
"dev-develop": "1.x-dev"
52+
}
53+
},
54+
"minimum-stability": "dev",
55+
"prefer-stable": true
5156
}

src/AbstractConfigProps.php

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ abstract class AbstractConfigProps implements ConfigPropsInterface
1111
{
1212
/** @var array <int, string> */
1313
public array $missingProps = [];
14+
public ?string $path = null;
1415
public ?string $test = null;
1516

17+
private array $propDesc = [];
18+
private static array $childPropCache = [];
19+
1620
/**
1721
* Hydrate the properties/object with expected data, and handle unexpected data
1822
*
@@ -65,6 +69,32 @@ public function setProp(string $key, mixed $value): self
6569
return $this;
6670
}
6771

72+
73+
/**
74+
* Add description to prop
75+
*
76+
* @param string $key
77+
* @param string $desc
78+
* @return $this
79+
*/
80+
protected function setPropDesc(string $key, string $desc): self
81+
{
82+
$this->propDesc[$key] = $desc;
83+
return $this;
84+
}
85+
86+
/**
87+
* Get description to prop
88+
*
89+
* @param string $key
90+
* @param string $desc
91+
* @return string
92+
*/
93+
public function getPropDesc(string $key): string
94+
{
95+
return $this->propDesc[$key] ?? "";
96+
}
97+
6898
/**
6999
* Set multiple config props
70100
*
@@ -100,15 +130,22 @@ public function get(string $key): mixed
100130
return ($newKey !== false) ? $this->{$newKey} : null;
101131
}
102132

103-
/**
104-
* Return props object as array
105-
*
106-
* @return array
107-
*/
108-
public function toArray(): array
109-
{
110-
return get_object_vars($this);
111-
}
133+
/**
134+
* Return public properties defined on the concrete class
135+
*/
136+
public function toArray(): array
137+
{
138+
$vars = get_object_vars($this);
139+
140+
if (!isset(self::$childPropCache[static::class])) {
141+
$childDefaults = get_class_vars(static::class);
142+
$baseDefaults = get_class_vars(self::class);
143+
144+
self::$childPropCache[static::class] = array_diff_key($childDefaults, $baseDefaults);
145+
}
146+
147+
return array_intersect_key($vars, self::$childPropCache[static::class]);
148+
}
112149

113150
/**
114151
* Get value as bool value

src/AbstractKernel.php

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,20 @@
1515
namespace MaplePHP\Emitron;
1616

1717
use MaplePHP\Container\Reflection;
18+
use MaplePHP\Emitron\Contracts\AppInterface;
1819
use MaplePHP\Emitron\Contracts\DispatchConfigInterface;
1920
use MaplePHP\Emitron\Contracts\EmitterInterface;
2021
use MaplePHP\Emitron\Contracts\KernelInterface;
2122
use MaplePHP\Emitron\Emitters\CliEmitter;
2223
use MaplePHP\Emitron\Emitters\HttpEmitter;
24+
use MaplePHP\Http\Interfaces\PathInterface;
2325
use MaplePHP\Http\ResponseFactory;
2426
use MaplePHP\Http\Stream;
2527
use Psr\Container\ContainerInterface;
2628
use Psr\Http\Message\ResponseInterface;
2729
use Psr\Http\Message\ServerRequestInterface;
2830
use Psr\Http\Message\StreamInterface;
31+
use Psr\Http\Server\RequestHandlerInterface;
2932

3033
abstract class AbstractKernel implements KernelInterface
3134
{
@@ -118,24 +121,41 @@ public function getDispatchConfig(): DispatchConfigInterface
118121
*
119122
* @param ServerRequestInterface $request
120123
* @param StreamInterface $stream
124+
* @param RequestHandlerInterface $finalHandler
121125
* @param array $middlewares
122126
* @return ResponseInterface
123127
* @throws \ReflectionException
124128
*/
125129
protected function initRequestHandler(
126130
ServerRequestInterface $request,
127131
StreamInterface $stream,
132+
PathInterface $path,
133+
RequestHandlerInterface $finalHandler,
128134
array $middlewares = []
129-
) : ResponseInterface {
130-
$factory = new ResponseFactory($stream);
135+
): ResponseInterface {
136+
131137
$this->bindInterfaces([
132-
"ContainerInterface" => $this->container, "RequestInterface" => $request,
133-
"ServerRequestInterface" => $request, "StreamInterface" => $stream,
138+
"ContainerInterface" => $this->container,
139+
"RequestInterface" => $request,
140+
"ServerRequestInterface" => $request,
141+
"StreamInterface" => $stream,
142+
"PathInterface" => $path
134143
]);
144+
135145
$middlewares = array_merge($this->userMiddlewares, $middlewares);
136-
$handler = new RequestHandler($middlewares, $factory);
137-
$response = $handler->handle($request);
138-
$this->bindInterfaces(["ResponseInterface" => $response]);
146+
$handler = new RequestHandler($middlewares, $finalHandler);
147+
$app = $this->container->has("app") ? $this->container->get("app") : null;
148+
149+
ob_start();
150+
$response = $handler->handle($request);
151+
$output = ob_get_clean();
152+
153+
if((string)$output !== "" && ($app instanceof AppInterface && !$app->isProd())) {
154+
throw new \RuntimeException(
155+
'Unexpected output detected during request dispatch. Controllers must write to the response body instead of using echo.'
156+
);
157+
}
158+
139159
return $response;
140160
}
141161

src/Configs/ConfigPropsFactory.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MaplePHP\Emitron\Configs;
6+
7+
use MaplePHP\Emitron\AbstractConfigProps;
8+
use MaplePHP\Emitron\Contracts\ConfigPropsInterface;
9+
10+
class ConfigPropsFactory
11+
{
12+
/**
13+
* Get expected instance of Config Props
14+
*
15+
* @param array $props
16+
* @param string|null $configProps
17+
* @return ConfigPropsInterface
18+
*/
19+
public static function create(array $props, ?string $configProps = null): ConfigPropsInterface
20+
{
21+
$override = ($configProps !== null) ? $configProps : '\\Configs\\CliOptions';
22+
$default = \MaplePHP\Unitary\Config\ConfigProps::class;
23+
$name = (class_exists($override)) ? $override : $default;
24+
if (!is_subclass_of($name, ConfigPropsInterface::class)) {
25+
$name = $default;
26+
}
27+
if (!class_exists($name)) {
28+
return self::resolver($props);
29+
}
30+
return new $name($props);
31+
}
32+
33+
/**
34+
* Will resolve minimum required config dependencies
35+
*
36+
* @param array $props
37+
* @return ConfigPropsInterface
38+
*/
39+
private static function resolver(array $props): ConfigPropsInterface
40+
{
41+
return new class($props) extends AbstractConfigProps {
42+
protected function propsHydration(bool|string $key, mixed $value): void
43+
{
44+
// Intentionally no-op (or implement minimal hydration rules)
45+
}
46+
};
47+
}
48+
}

src/Contracts/AppInterface.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MaplePHP\Emitron\Contracts;
6+
7+
use MaplePHP\Core\Support\Dir;
8+
9+
interface AppInterface
10+
{
11+
12+
/**
13+
* This is a single to set App globals
14+
*
15+
* @param Dir $dir
16+
* @param array $config
17+
* @return self
18+
*/
19+
public static function boot(Dir $dir, array $config = []): self;
20+
21+
/**
22+
* Get App singleton instance
23+
*
24+
* @return self
25+
*/
26+
public static function get(): self;
27+
28+
/**
29+
* Check if the environment is in prod
30+
*
31+
* @return bool
32+
*/
33+
public function isProd(): bool;
34+
35+
/**
36+
* Check if the environment is in stage
37+
*
38+
* @return bool
39+
*/
40+
public function isStage(): bool;
41+
42+
/**
43+
* Check if the environment is in test
44+
*
45+
* @return bool
46+
*/
47+
public function isTest(): bool;
48+
49+
/**
50+
* Check if the environment is in dev
51+
*
52+
* @return bool
53+
*/
54+
public function isDev(): bool;
55+
56+
/**
57+
* Get current Environment
58+
*
59+
* @return string
60+
*/
61+
public function env(): string;
62+
63+
/**
64+
* Get core/boot dir where code app boot originate
65+
*
66+
* @return string
67+
*/
68+
public function coreDir(): string;
69+
70+
/**
71+
* Get the app core Dir instance
72+
*
73+
* @return Dir
74+
*/
75+
public function dir(): Dir;
76+
77+
}

0 commit comments

Comments
 (0)