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
26 changes: 26 additions & 0 deletions .github/workflows/tests-unit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Run Unit Tests

on:
pull_request:
types:
- "opened"
- "synchronize"
branches:
- master

jobs:
phpunit:
name: PHP Unit Test
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Create PHP Environment
run: docker compose up -d

- name: Run Composer Install
run: docker exec app_engine_php_apache composer install

- name: Run Unit Tests
run: docker exec app_engine_php_apache vendor/bin/phpunit
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Subtext App Engine

## A tiny framework for building PHP based web applications.
![workflow](https://github.com/subtext/app-engine/actions/workflows/tests-unit.yml/badge.svg)

### Installation
```shell
Expand All @@ -25,4 +26,4 @@ You would then simply need to create the HomeController class extending from the
Subtext\Base\Controller class. Add models and views as necessary to create any
dependencies for your controllers as they will be the primary means of manipulating
the application. For more information on configuring routes see:
https://symfony.com/doc/4.3/components/routing.html
https://symfony.com/doc/4.3/components/routing.html
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"vlucas/phpdotenv": "^5.6",
"laravel/pint": "^1.22",
"robmorgan/phinx": "^0.16.8",
"fakerphp/faker": "^1.24"
"fakerphp/faker": "^1.24",
"mikey179/vfsstream": "^1.6.12"
}
}
57 changes: 54 additions & 3 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 11 additions & 45 deletions config/unit.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,22 @@
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger;
use Subtext\AppEngine\Controllers\Factory;
use Subtext\AppEngine\Controller;
use Subtext\AppEngine\Controllers\Loader;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Loader\PhpFileLoader;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Router;
use Symfony\Component\HttpFoundation\Response;

use function DI\factory;

return [
Factory::class => factory(function (ContainerInterface $c) {
return new Factory(function (string $class) use ($c) {
if (!$c->has($class)) {
throw new RouteNotFoundException();
}
return $c->get($class);
});
}),
Loader::class => factory(function (
Router $router,
Request $request,
Factory $factory
) {
$params = Loader::resolveController($router, $request);
$controller = ($params['_controller'] ?? null);
if (!$controller) {
throw new RouteNotFoundException();
}
return Loader::create($factory->get($controller), $params);
Loader::class => factory(function () {
return Loader::create(
new class () extends Controller {
public function execute(mixed $params): Response
{
return new Response('Hello World');
}
}, []
);
}),
LoggerInterface::class => factory(function(ContainerInterface $c) {
$logger = new Logger('debug');
Expand All @@ -52,23 +37,4 @@

return $logger;
}),
Request::class => factory([Request::class, 'createFromGlobals']),
RequestContext::class => factory(
function (ContainerInterface $c) {
$context = new RequestContext();
$context->fromRequest($c->get(Request::class));

return $context;
}
),
Router::class => factory(
function (ContainerInterface $c) {
return new Router(
new PhpFileLoader(new FileLocator([dirname(__DIR__)])),
'config/routes.php',
[],
$c->get(RequestContext::class)
);
}
),
];
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ services:
VIRTUAL_ADMIN: alonzo.turner@subtext.productions
PHP_IDE_CONFIG: serverName=AppEngine
APP_CONFIG: unit.php
working_dir: /var/www
volumes:
- ./:/var/www
ports:
- 80:80

2 changes: 1 addition & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/12.2/phpunit.xsd"
bootstrap="tests/unit/bootstrap.php"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
executionOrder="depends,defects"
beStrictAboutOutputDuringTests="true"
Expand Down
2 changes: 1 addition & 1 deletion src/Controllers/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

class Factory
{
public function __construct(private Closure $resolver)
public function __construct(private readonly Closure $resolver)
{}

public function get(string $class): Controller
Expand Down
18 changes: 18 additions & 0 deletions src/Controllers/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,32 @@

class Loader
{
/**
* Can only be instantiated using the public static create method
*
* @param Controller $controller
* @param mixed $params
*/
private function __construct(private Controller $controller, private mixed $params)
{}

/**
* @param Router $router The symfony router component
* @param Request $request The symfony request component
*
* @return array The route parameters
*/
public static function resolveController(Router $router, Request $request): array
{
return $router->matchRequest($request);
}

/**
* @param Controller $controller The controller to be passed to the app
* @param mixed $params The route params as an array
*
* @return self
*/
public static function create(Controller $controller, mixed $params): self
{
return new self($controller, $params);
Expand Down
44 changes: 15 additions & 29 deletions tests/unit/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use RuntimeException;
use Subtext\AppEngine\Application;
use Subtext\AppEngine\Controller;
use Subtext\AppEngine\Controllers\Loader;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Router;
Expand Down Expand Up @@ -37,24 +38,17 @@ public function testExecute(): void
$controller = $this->createMock(Controller::class);
$controller->expects($this->once())
->method('execute')
->with([])
->willReturn($response);
$container = $this->createMock(Container::class);
$container->expects($this->once())
->method('has')
->with('UnitController')
->willReturn(true);
$container->expects($this->once())
->method('get')
->with('UnitController')
$loader = $this->createMock(Loader::class);
$loader->expects($this->once())
->method('getController')
->willReturn($controller);
$loader->expects($this->once())
->method('getParams')
->willReturn([]);
$logger = $this->createMock(LoggerInterface::class);
$request = $this->createMock(Request::class);
$router = $this->createMock(Router::class);
$router->expects($this->once())
->method('matchRequest')
->with($request)
->willReturn(['_controller' => 'UnitController']);
$app = new Application($container, $logger, $request, $router);
$app = new Application($loader, $logger);
ob_start();
$app->execute();
$actual = ob_get_clean();
Expand All @@ -67,22 +61,14 @@ public function testExecute(): void
* @covers ::validateRequestUri
* @throws Exception
*/
public function testExecuteWillThrowMissingControllerException(): void
public function testExecuteWillCatchAnyException(): void
{
$container = $this->createMock(Container::class);
$container->expects($this->once())
->method('has')
->with('UnitController')
->willReturn(false);
$loader = $this->createMock(Loader::class);
$loader->expects($this->once())
->method('getController')
->willThrowException(new ResourceNotFoundException());
$logger = $this->createMock(LoggerInterface::class);
$request = $this->createMock(Request::class);
$router = $this->createMock(Router::class);
$router->expects($this->once())
->method('matchRequest')
->with($request)
->willReturn(['_controller' => 'UnitController']);

$app = new Application($container, $logger, $request, $router);
$app = new Application($loader, $logger);
try {
$app->execute();
}catch (Throwable $e) {
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/BootstrapTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function testGetContainer()
public function testGetApplication()
{
$rootPath = dirname(__DIR__, 2);
$bootstrap = new Bootstrap($rootPath);
$bootstrap = new Bootstrap($rootPath, 'unit.php');
$app = $bootstrap->application;
$this->assertInstanceOf(Application::class, $app);
}
Expand Down
34 changes: 34 additions & 0 deletions tests/unit/Controllers/FactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Subtext\AppEngine\Test\Unit\Controllers;

use PHPUnit\Framework\TestCase;
use RuntimeException;
use Subtext\AppEngine\Controller;
use Subtext\AppEngine\Controllers\Factory;
use Symfony\Component\HttpFoundation\Response;

class FactoryTest extends TestCase
{
public function testCanPassControllerThroughClosure(): void
{
$unit = new Factory(function(string $class) {
return new class () extends Controller {
public function execute(mixed $params): Response
{
return new Response();
}
};
});
$this->assertInstanceOf(Controller::class, $unit->get(''));
}

public function testWillThrowRuntimeException(): void
{
$unit = new Factory(function(string $class) {
return null;
});
$this->expectException(RuntimeException::class);
$unit->get('foobar');
}
}
Loading