Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: PHP Unitary

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
env:
COMPOSER_ROOT_VERSION: 1.x-dev

steps:
- uses: actions/checkout@v4

- name: Cache Composer packages
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-

- name: Install dependencies
run: composer install --prefer-dist --no-progress

- name: Run test suite
run: php vendor/bin/unitary
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,19 @@ composer require maplephp/emitron
Emitron includes a robust request handler that executes PSR-15 middlewares in sequence, returning a fully PSR-7 compliant response.

```php
use MaplePHP\Emitron\RequestHandler;use MaplePHP\Http\Environment;use MaplePHP\Http\ServerRequest;use MaplePHP\Http\Uri;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use MaplePHP\Emitron\RequestHandler;
use MaplePHP\Emitron\Emitters\HttpEmitter;
use MaplePHP\Http\Environment;
use MaplePHP\Http\ServerRequest;
use MaplePHP\Http\Uri;
use MaplePHP\Emitron\Middlewares\{
ContentLengthMiddleware,
GzipMiddleware,
HeadRequestMiddleware
};
use App\Controllers\MyController;

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

// Run the middleware stack
$handler = new RequestHandler($middlewares, $factory);


$factory = new ResponseFactory($bodyStream);
// $finalHandler = new ControllerRequestHandler($factory, [MyController::class, "index"]);
$finalHandler = new ControllerRequestHandler($factory, function(RequestInterface $request, ResponseInterface $response) {
$response->getBody()->write("Lorem ipsum dolor sit amet, consectetur adipiscing elit.");
return $response;
});

$handler = new RequestHandler($middlewares, $finalHandler);
$response = $handler->handle($request);

// Emit the execute headers and response correctly
//$emit = new CliEmitter($response, $request);
$emit = new HttpEmitter();
$emit->emit($response, $request);

```

Each middleware conforms to `Psr\Http\Server\MiddlewareInterface`, allowing you to plug in your own or third-party middlewares with no additional setup.
Expand Down
17 changes: 11 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"homepage": "https://wazabii.se"
}
],
"scripts": {
"test": "php vendor/bin/unitary"
},
"require": {
"php": ">=8.2",
"psr/http-server-middleware": "^1.0",
Expand All @@ -37,15 +40,17 @@
"require-dev": {
"maplephp/unitary": "^2.0"
},
"extra": {
"branch-alias": {
"dev-main": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"MaplePHP\\Emitron\\": "src"
}
},
"minimum-stability": "dev"
"extra": {
"branch-alias": {
"dev-main": "1.x-dev",
"dev-develop": "1.x-dev"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}
55 changes: 46 additions & 9 deletions src/AbstractConfigProps.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ abstract class AbstractConfigProps implements ConfigPropsInterface
{
/** @var array <int, string> */
public array $missingProps = [];
public ?string $path = null;
public ?string $test = null;

private array $propDesc = [];
private static array $childPropCache = [];

/**
* Hydrate the properties/object with expected data, and handle unexpected data
*
Expand Down Expand Up @@ -65,6 +69,32 @@ public function setProp(string $key, mixed $value): self
return $this;
}


/**
* Add description to prop
*
* @param string $key
* @param string $desc
* @return $this
*/
protected function setPropDesc(string $key, string $desc): self
{
$this->propDesc[$key] = $desc;
return $this;
}

/**
* Get description to prop
*
* @param string $key
* @param string $desc
* @return string
*/
public function getPropDesc(string $key): string
{
return $this->propDesc[$key] ?? "";
}

/**
* Set multiple config props
*
Expand Down Expand Up @@ -100,15 +130,22 @@ public function get(string $key): mixed
return ($newKey !== false) ? $this->{$newKey} : null;
}

/**
* Return props object as array
*
* @return array
*/
public function toArray(): array
{
return get_object_vars($this);
}
/**
* Return public properties defined on the concrete class
*/
public function toArray(): array
{
$vars = get_object_vars($this);

if (!isset(self::$childPropCache[static::class])) {
$childDefaults = get_class_vars(static::class);
$baseDefaults = get_class_vars(self::class);

self::$childPropCache[static::class] = array_diff_key($childDefaults, $baseDefaults);
}

return array_intersect_key($vars, self::$childPropCache[static::class]);
}

/**
* Get value as bool value
Expand Down
34 changes: 27 additions & 7 deletions src/AbstractKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@
namespace MaplePHP\Emitron;

use MaplePHP\Container\Reflection;
use MaplePHP\Emitron\Contracts\AppInterface;
use MaplePHP\Emitron\Contracts\DispatchConfigInterface;
use MaplePHP\Emitron\Contracts\EmitterInterface;
use MaplePHP\Emitron\Contracts\KernelInterface;
use MaplePHP\Emitron\Emitters\CliEmitter;
use MaplePHP\Emitron\Emitters\HttpEmitter;
use MaplePHP\Http\Interfaces\PathInterface;
use MaplePHP\Http\ResponseFactory;
use MaplePHP\Http\Stream;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Server\RequestHandlerInterface;

abstract class AbstractKernel implements KernelInterface
{
Expand Down Expand Up @@ -118,24 +121,41 @@ public function getDispatchConfig(): DispatchConfigInterface
*
* @param ServerRequestInterface $request
* @param StreamInterface $stream
* @param RequestHandlerInterface $finalHandler
* @param array $middlewares
* @return ResponseInterface
* @throws \ReflectionException
*/
protected function initRequestHandler(
ServerRequestInterface $request,
StreamInterface $stream,
PathInterface $path,
RequestHandlerInterface $finalHandler,
array $middlewares = []
) : ResponseInterface {
$factory = new ResponseFactory($stream);
): ResponseInterface {

$this->bindInterfaces([
"ContainerInterface" => $this->container, "RequestInterface" => $request,
"ServerRequestInterface" => $request, "StreamInterface" => $stream,
"ContainerInterface" => $this->container,
"RequestInterface" => $request,
"ServerRequestInterface" => $request,
"StreamInterface" => $stream,
"PathInterface" => $path
]);

$middlewares = array_merge($this->userMiddlewares, $middlewares);
$handler = new RequestHandler($middlewares, $factory);
$response = $handler->handle($request);
$this->bindInterfaces(["ResponseInterface" => $response]);
$handler = new RequestHandler($middlewares, $finalHandler);
$app = $this->container->has("app") ? $this->container->get("app") : null;

ob_start();
$response = $handler->handle($request);
$output = ob_get_clean();

if((string)$output !== "" && ($app instanceof AppInterface && !$app->isProd())) {
throw new \RuntimeException(
'Unexpected output detected during request dispatch. Controllers must write to the response body instead of using echo.'
);
}

return $response;
}

Expand Down
48 changes: 48 additions & 0 deletions src/Configs/ConfigPropsFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace MaplePHP\Emitron\Configs;

use MaplePHP\Emitron\AbstractConfigProps;
use MaplePHP\Emitron\Contracts\ConfigPropsInterface;

class ConfigPropsFactory
{
/**
* Get expected instance of Config Props
*
* @param array $props
* @param string|null $configProps
* @return ConfigPropsInterface
*/
public static function create(array $props, ?string $configProps = null): ConfigPropsInterface
{
$override = ($configProps !== null) ? $configProps : '\\Configs\\CliOptions';
$default = \MaplePHP\Unitary\Config\ConfigProps::class;
$name = (class_exists($override)) ? $override : $default;
if (!is_subclass_of($name, ConfigPropsInterface::class)) {
$name = $default;
}
if (!class_exists($name)) {
return self::resolver($props);
}
return new $name($props);
}

/**
* Will resolve minimum required config dependencies
*
* @param array $props
* @return ConfigPropsInterface
*/
private static function resolver(array $props): ConfigPropsInterface
{
return new class($props) extends AbstractConfigProps {
protected function propsHydration(bool|string $key, mixed $value): void
{
// Intentionally no-op (or implement minimal hydration rules)
}
};
}
}
77 changes: 77 additions & 0 deletions src/Contracts/AppInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace MaplePHP\Emitron\Contracts;

use MaplePHP\Core\Support\Dir;

interface AppInterface
{

/**
* This is a single to set App globals
*
* @param Dir $dir
* @param array $config
* @return self
*/
public static function boot(Dir $dir, array $config = []): self;

/**
* Get App singleton instance
*
* @return self
*/
public static function get(): self;

/**
* Check if the environment is in prod
*
* @return bool
*/
public function isProd(): bool;

/**
* Check if the environment is in stage
*
* @return bool
*/
public function isStage(): bool;

/**
* Check if the environment is in test
*
* @return bool
*/
public function isTest(): bool;

/**
* Check if the environment is in dev
*
* @return bool
*/
public function isDev(): bool;

/**
* Get current Environment
*
* @return string
*/
public function env(): string;

/**
* Get core/boot dir where code app boot originate
*
* @return string
*/
public function coreDir(): string;

/**
* Get the app core Dir instance
*
* @return Dir
*/
public function dir(): Dir;

}
Loading