Initial commit.

This commit is contained in:
Brandon Savage 2022-11-22 15:19:13 -05:00
commit 63686360bc
No known key found for this signature in database
GPG key ID: F25174ADD7D627E9
49 changed files with 8673 additions and 0 deletions

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
/.phpcs-cache
/.phpunit.result.cache
/clover.xml
/coveralls-upload.json
/phpunit.xml
/vendor/
docker-compose.override.yml
/assets/output/*
/node_modules/
/logs/error.log

27
Makefile Normal file
View file

@ -0,0 +1,27 @@
.PHONY: *

OPTS=

unit:
docker compose run --rm webapp bash -c "vendor/bin/phpunit --testsuite=unit ${OPTS}"

functional:
docker compose run --rm webapp bash -c "vendor/bin/phpunit --testsuite=functional ${OPTS}"

test:
docker compose run --rm webapp bash -c "vendor/bin/phpunit ${OPTS}"

acceptance:
docker compose run --rm webapp bash -c "vendor/bin/behat -vvv ${OPTS}"

psalm:
docker compose run --rm webapp bash -c "vendor/bin/psalm --show-info=false ${OPTS}"

css:
docker compose run --rm node bash -c "npx tailwindcss -i ./assets/source/style.css -o ./assets/output/style.css"

css-watch:
docker compose run --rm node bash -c "npx tailwindcss -i ./assets/source/style.css -o ./assets/output/style.css --watch"

install-node:
docker compose run --rm node bash -c "npm install"

3
assets/source/style.css Normal file
View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

14
behat.yml Normal file
View file

@ -0,0 +1,14 @@
default:
autoload:
'': '%paths.base%/tests/acceptance/src'
suites:
default:
autowire: true
services: "@psr_container"
paths:
- '%paths.base%/tests/acceptance/features'
contexts:
- AppTest\Acceptance\SampleContext
extensions:
Roave\BehatPsrContainer\PsrContainerExtension:
container: '%paths.base%/tests/acceptance/container.php'

View file

@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

chdir(__DIR__ . '/../');

require 'vendor/autoload.php';

$config = include 'config/config.php';

if (! isset($config['config_cache_path'])) {
echo "No configuration cache path found" . PHP_EOL;
exit(0);
}

if (! file_exists($config['config_cache_path'])) {
printf(
"Configured config cache file '%s' not found%s",
$config['config_cache_path'],
PHP_EOL
);
exit(0);
}

if (false === unlink($config['config_cache_path'])) {
printf(
"Error removing config cache file '%s'%s",
$config['config_cache_path'],
PHP_EOL
);
exit(1);
}

printf(
"Removed configured config cache file '%s'%s",
$config['config_cache_path'],
PHP_EOL
);
exit(0);

69
composer.json Normal file
View file

@ -0,0 +1,69 @@
{
"name": "tailwindsllc/project-skeleton",
"description": "A project skeleton for Tailwinds projects.",
"config": {
"sort-packages": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"composer/package-versions-deprecated": true,
"laminas/laminas-component-installer": true
}
},
"require": {
"php": "~8.0.0 || ~8.1.0",
"composer/package-versions-deprecated": "^1.10.99",
"laminas/laminas-component-installer": "^2.6",
"laminas/laminas-config-aggregator": "^1.6",
"laminas/laminas-diactoros": "^2.7",
"laminas/laminas-servicemanager": "^3.4",
"laminas/laminas-stdlib": "^3.6",
"mezzio/mezzio": "^3.7",
"mezzio/mezzio-fastroute": "^3.0.3",
"mezzio/mezzio-helpers": "^5.7",
"mezzio/mezzio-platesrenderer": "^2.2",
"monolog/monolog": "^3.2",
"webmozart/assert": "^1.11"
},
"require-dev": {
"behat/behat": "^3.11",
"filp/whoops": "^2.7.1",
"laminas/laminas-development-mode": "^3.3.0",
"mezzio/mezzio-tooling": "^2.1",
"phpunit/phpunit": "^9.5.11",
"phpstan/phpstan": "^1.9",
"roave/behat-psr11extension": "^2.2",
"roave/security-advisories": "dev-master"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"AppTest\\Unit\\": "tests/unit/src",
"AppTest\\Functional\\": "tests/functional/src",
"AppTest\\Acceptance\\": "tests/acceptance/src"
}
},
"scripts": {
"post-create-project-cmd": [
"@development-enable"
],
"development-disable": "laminas-development-mode disable",
"development-enable": "laminas-development-mode enable",
"development-status": "laminas-development-mode status",
"mezzio": "laminas --ansi",
"check": [
"@cs-check",
"@test"
],
"clear-config-cache": "php bin/clear-config-cache.php",
"enable-codestandard": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run",
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"serve": "php -S 0.0.0.0:8080 -t public/",
"test": "phpunit --colors=always",
"test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
}
}

6344
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

1
config/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
development.config.php

2
config/autoload/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
local.php
*.local.php

View file

@ -0,0 +1,24 @@
<?php

return [
/*
* Logging Configuration
*/
'logging' => [
'path' => './logs/',
'error' => [
'file' => 'error.log',
'level' => \Monolog\Level::Debug,
],
],

/*
* Plates Configuration
*/
'templates' => [
'extension' => 'php',
'paths' => [
'app' => './templates/app',
],
],
];

View file

@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

return [
// Provides application-wide services.
// We recommend using fully-qualified class names whenever possible as
// service names.
'dependencies' => [
// Use 'aliases' to alias a service name to another service. The
// key is the alias name, the value is the service to which it points.
'aliases' => [
// Fully\Qualified\ClassOrInterfaceName::class => Fully\Qualified\ClassName::class,
],
// Use 'invokables' for constructor-less services, or services that do
// not require arguments to the constructor. Map a service name to the
// class name.
'invokables' => [
// Fully\Qualified\InterfaceName::class => Fully\Qualified\ClassName::class,
],
// Use 'factories' for services provided by callbacks/factory classes.
'factories' => [
// Fully\Qualified\ClassName::class => Fully\Qualified\FactoryName::class,
],
],
];

View file

@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

// phpcs:disable PSR12.Files.FileHeader.IncorrectOrder

/**
* Development-only configuration.
*
* Put settings you want enabled when under development mode in this file, and
* check it into your repository.
*
* Developers on your team will then automatically enable them by calling on
* `composer development-enable`.
*/

use Mezzio\Container;
use Mezzio\Middleware\ErrorResponseGenerator;

return [
'dependencies' => [
'factories' => [
ErrorResponseGenerator::class => Container\WhoopsErrorResponseGeneratorFactory::class,
'Mezzio\Whoops' => Container\WhoopsFactory::class,
'Mezzio\WhoopsPageHandler' => Container\WhoopsPageHandlerFactory::class,
],
],
'whoops' => [
'json_exceptions' => [
'display' => true,
'show_trace' => true,
'ajax_only' => true,
],
],
];

View file

@ -0,0 +1,13 @@
<?php

/**
* Local configuration.
*
* Copy this file to `local.php` and change its settings as required.
* `local.php` is ignored by git and safe to use for local and sensitive data like usernames and passwords.
*/

declare(strict_types=1);

return [
];

View file

@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

use Laminas\ConfigAggregator\ConfigAggregator;

return [
// Toggle the configuration cache. Set this to boolean false, or remove the
// directive, to disable configuration caching. Toggling development mode
// will also disable it by default; clear the configuration cache using
// `composer clear-config-cache`.
ConfigAggregator::ENABLE_CACHE => true,

// Enable debugging; typically used to provide debugging information within templates.
'debug' => false,
'mezzio' => [
// Provide templates for the error handling middleware to use when
// generating responses.
'error_handler' => [
'template_404' => 'error::404',
'template_error' => 'error::error',
],
],
];

42
config/config.php Normal file
View file

@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

use Laminas\ConfigAggregator\ArrayProvider;
use Laminas\ConfigAggregator\ConfigAggregator;
use Laminas\ConfigAggregator\PhpFileProvider;
use Mezzio\Helper\ConfigProvider;

// To enable or disable caching, set the `ConfigAggregator::ENABLE_CACHE` boolean in
// `config/autoload/local.php`.
$cacheConfig = [
'config_cache_path' => 'data/cache/config-cache.php',
];

$aggregator = new ConfigAggregator([
\Mezzio\Tooling\ConfigProvider::class,
\Mezzio\Plates\ConfigProvider::class,
\Mezzio\Helper\ConfigProvider::class,
\Mezzio\Router\FastRouteRouter\ConfigProvider::class,
\Laminas\HttpHandlerRunner\ConfigProvider::class,
// Include cache configuration
new ArrayProvider($cacheConfig),
ConfigProvider::class,
\Mezzio\ConfigProvider::class,
\Mezzio\Router\ConfigProvider::class,
\Laminas\Diactoros\ConfigProvider::class,

\App\ConfigProvider::class,

// Load application config in a pre-defined order in such a way that local settings
// overwrite global settings. (Loaded as first to last):
// - `global.php`
// - `*.global.php`
// - `local.php`
// - `*.local.php`
new PhpFileProvider(realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php'),
// Load development config if it exists
new PhpFileProvider(realpath(__DIR__) . '/development.config.php'),
], $cacheConfig['config_cache_path']);

return $aggregator->getMergedConfig();

14
config/container.php Normal file
View file

@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

use Laminas\ServiceManager\ServiceManager;

// Load configuration
$config = require __DIR__ . '/config.php';

$dependencies = $config['dependencies'];
$dependencies['services']['config'] = $config;

// Build container
return new ServiceManager($dependencies);

View file

@ -0,0 +1,31 @@
<?php

/**
* File required to allow enablement of development mode.
*
* For use with the laminas-development-mode tool.
*
* Usage:
* $ composer development-disable
* $ composer development-enable
* $ composer development-status
*
* DO NOT MODIFY THIS FILE.
*
* Provide your own development-mode settings by editing the file
* `config/autoload/development.local.php.dist`.
*
* Because this file is aggregated last, it simply ensures:
*
* - The `debug` flag is _enabled_.
* - Configuration caching is _disabled_.
*/

declare(strict_types=1);

use Laminas\ConfigAggregator\ConfigAggregator;

return [
'debug' => true,
ConfigAggregator::ENABLE_CACHE => false,
];

77
config/pipeline.php Normal file
View file

@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

use Laminas\Stratigility\Middleware\ErrorHandler;
use Mezzio\Application;
use Mezzio\Handler\NotFoundHandler;
use Mezzio\Helper\ServerUrlMiddleware;
use Mezzio\Helper\UrlHelperMiddleware;
use Mezzio\MiddlewareFactory;
use Mezzio\Router\Middleware\DispatchMiddleware;
use Mezzio\Router\Middleware\ImplicitHeadMiddleware;
use Mezzio\Router\Middleware\ImplicitOptionsMiddleware;
use Mezzio\Router\Middleware\MethodNotAllowedMiddleware;
use Mezzio\Router\Middleware\RouteMiddleware;
use Psr\Container\ContainerInterface;

/**
* Setup middleware pipeline:
*/

return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container): void {
// The error handler should be the first (most outer) middleware to catch
// all Exceptions.
$app->pipe(ErrorHandler::class);
$app->pipe(ServerUrlMiddleware::class);

// Pipe more middleware here that you want to execute on every request:
// - bootstrapping
// - pre-conditions
// - modifications to outgoing responses
//
// Piped Middleware may be either callables or service names. Middleware may
// also be passed as an array; each item in the array must resolve to
// middleware eventually (i.e., callable or service name).
//
// Middleware can be attached to specific paths, allowing you to mix and match
// applications under a common domain. The handlers in each middleware
// attached this way will see a URI with the matched path segment removed.
//
// i.e., path of "/api/member/profile" only passes "/member/profile" to $apiMiddleware
// - $app->pipe('/api', $apiMiddleware);
// - $app->pipe('/docs', $apiDocMiddleware);
// - $app->pipe('/files', $filesMiddleware);

// Register the routing middleware in the middleware pipeline.
// This middleware registers the Mezzio\Router\RouteResult request attribute.
$app->pipe(RouteMiddleware::class);

// The following handle routing failures for common conditions:
// - HEAD request but no routes answer that method
// - OPTIONS request but no routes answer that method
// - method not allowed
// Order here matters; the MethodNotAllowedMiddleware should be placed
// after the Implicit*Middleware.
$app->pipe(ImplicitHeadMiddleware::class);
$app->pipe(ImplicitOptionsMiddleware::class);
$app->pipe(MethodNotAllowedMiddleware::class);

// Seed the UrlHelper with the routing results:
$app->pipe(UrlHelperMiddleware::class);

// Add more middleware here that needs to introspect the routing results; this
// might include:
//
// - route-based authentication
// - route-based validation
// - etc.

// Register the dispatch middleware in the middleware pipeline
$app->pipe(DispatchMiddleware::class);

// At this point, if no Response is returned by any middleware, the
// NotFoundHandler kicks in; alternately, you can provide other fallback
// middleware to execute.
$app->pipe(NotFoundHandler::class);
};

29
config/routes.php Normal file
View file

@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

use Mezzio\Application;
use Mezzio\MiddlewareFactory;
use Psr\Container\ContainerInterface;

/**
* FastRoute route configuration
*
* @see https://github.com/nikic/FastRoute
*
* Setup routes with a single request method:
*
* $app->get('/', App\Handler\HomePageHandler::class, 'home');
* $app->post('/album', App\Handler\AlbumCreateHandler::class, 'album.create');
* $app->put('/album/{id:\d+}', App\Handler\AlbumUpdateHandler::class, 'album.put');
* $app->patch('/album/{id:\d+}', App\Handler\AlbumUpdateHandler::class, 'album.patch');
* $app->delete('/album/{id:\d+}', App\Handler\AlbumDeleteHandler::class, 'album.delete');
*
* Or with multiple request methods:
*
* $app->route('/contact', App\Handler\ContactHandler::class, ['GET', 'POST', ...], 'contact');
*/

return static function (Application $app, MiddlewareFactory $factory, ContainerInterface $container): void {
$app->get('/', \App\TestPage::class, 'app.home');
};

4
data/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
*
!cache
!cache/.gitkeep
!.gitignore

0
data/cache/.gitkeep vendored Normal file
View file

59
docker-compose.yml Normal file
View file

@ -0,0 +1,59 @@
version: '3'

services:
nginx:
build:
context: .
dockerfile: ./docker/nginx/Dockerfile
ports:
- 80:80
- 443:443
volumes:
- .:/var/www/html
- ./docker/nginx/default:/etc/nginx/conf.d/default.conf
networks:
application:
aliases:
- 'application.app.local'

webapp:
build:
context: .
dockerfile: docker/webapp/Dockerfile
environment:
- XDEBUG_CONFIG=idekey=PHPSTORM start_with_request=true var_display_max_depth=-1 max_nesting_level=3000
- PHP_IDE_CONFIG=serverName=altimeter.local
- XDEBUG_MODE=develop,debug,coverage
- IN_TEST_MODE=true
volumes:
- .:/var/www/html
networks:
- application

postgres:
image: postgres:latest
environment:
- POSTGRES_PASSWORD=password
- PGDATA=/opt/pgdata
- POSTGRES_DB=application
ports:
- 5432:5432
volumes:
- postgresdata:/opt/pgdata
networks:
- application

node:
image: node:19
user: node
working_dir: /home/node/app
volumes:
- .:/home/node/app

networks:
application:
driver: "bridge"

volumes:
postgresdata:
"driver": "local"

9
docker/nginx/Dockerfile Normal file
View file

@ -0,0 +1,9 @@
FROM nginx:latest

RUN apt-get update && apt install -y wget libnss3-tools \
&& wget -O mkcert \
https://github.com/FiloSottile/mkcert/releases/download/v1.4.3/mkcert-v1.4.3-linux-amd64 \
&& chmod +x mkcert \
&& mv mkcert /usr/local/bin \
&& mkcert -install \
&& mkcert *.application.local

41
docker/nginx/default Normal file
View file

@ -0,0 +1,41 @@
server {
listen 80 default_server;
return 302 https://$host$request_uri;
}

server {
listen 443 ssl;
client_max_body_size 4M;
server_name application.local;

ssl_certificate /_wildcard.application.local.pem;
ssl_certificate_key /_wildcard.application.local-key.pem;


root /var/www/html/public/;

index index.html index.htm index.php;


charset utf-8;

location = /favicon.ico { log_not_found off; access_log off; }
location = /robots.txt { log_not_found off; access_log off; }

location / {
try_files $uri $uri/ /index.php$is_args$args;
}

location ~ \.php$ {
fastcgi_pass webapp:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}

error_page 404 /index.php;

location ~ /\.ht {
deny all;
}
}

18
docker/webapp/Dockerfile Normal file
View file

@ -0,0 +1,18 @@
FROM php:8.1-fpm

MAINTAINER Brandon Savage

RUN export DEBIAN_FRONTEND=noninteractive \
&& apt-get update \
&& apt-get install -y -q \
libpq-dev git zip libzip-dev libicu-dev

RUN docker-php-ext-install pdo \
&& docker-php-ext-install pdo_pgsql \
&& docker-php-ext-install zip \
&& docker-php-ext-install intl \
&& pecl install xdebug-3.1.4 \
&& docker-php-ext-enable xdebug \
&& docker-php-ext-enable pdo_pgsql

COPY --from=composer:2.1 /usr/bin/composer /usr/bin/composer

5
docker/webapp/php.ini Normal file
View file

@ -0,0 +1,5 @@
error_reporting=E_ALL & ~E_DEPRECATED

[xdebug]
xdebug.client_host=host.docker.internal
xdebug.mode=debug

0
logs/.gitkeep Normal file
View file

2
logs/error.log Normal file

File diff suppressed because one or more lines are too long

1368
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

6
package.json Normal file
View file

@ -0,0 +1,6 @@
{
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",
"tailwindcss": "^3.2.4"
}
}

32
phpcs.xml.dist Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd">

<arg name="basepath" value="."/>
<arg name="cache" value=".phpcs-cache"/>
<arg name="colors"/>
<arg name="extensions" value="php"/>
<arg name="parallel" value="80"/>

<!-- Show progress -->
<arg value="p"/>

<!-- Paths to check -->
<file>config</file>
<file>src</file>
<file>test</file>
<exclude-pattern>config/config.php</exclude-pattern>
<exclude-pattern>config/routes.php</exclude-pattern>

<!-- Include all rules from the Laminas Coding Standard -->
<rule ref="LaminasCodingStandard"/>

<rule ref="Squiz.Classes.ClassFileName.NoMatch">
<exclude-pattern>src/ConfigProvider.*.php</exclude-pattern>
</rule>

<rule ref="PSR12.Files.FileHeader.IncorrectOrder">
<exclude-pattern>config/pipeline.php</exclude-pattern>
<exclude-pattern>src/MezzioInstaller/Resources/config/routes-*.php</exclude-pattern>
</rule>
</ruleset>

5
phpstan.neon Normal file
View file

@ -0,0 +1,5 @@
parameters:
level: 6
paths:
- src
- tests

26
phpunit.xml.dist Normal file
View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="tests/bootstrap.php"
executionOrder="depends,defects"
beStrictAboutCoversAnnotation="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
failOnRisky="true"
failOnWarning="true"
verbose="true"
colors="true">
<testsuites>
<testsuite name="unit">
<directory suffix="Test.php">tests/unit</directory>
</testsuite>
<testsuite name="functional">
<directory suffix="Test.php">tests/functional</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>

30
public/index.php Normal file
View file

@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

// Delegate static file requests back to the PHP built-in webserver
if (PHP_SAPI === 'cli-server' && $_SERVER['SCRIPT_FILENAME'] !== __FILE__) {
return false;
}

chdir(dirname(__DIR__));
require 'vendor/autoload.php';

/**
* Self-called anonymous function that creates its own scope and keeps the global namespace clean.
*/
(function () {
/** @var \Psr\Container\ContainerInterface $container */
$container = require 'config/container.php';

/** @var \Mezzio\Application $app */
$app = $container->get(\Mezzio\Application::class);
$factory = $container->get(\Mezzio\MiddlewareFactory::class);

// Execute programmatic/declarative middleware pipeline and routing
// configuration statements
(require 'config/pipeline.php')($app, $factory, $container);
(require 'config/routes.php')($app, $factory, $container);

$app->run();
})();

1
public/style.css Symbolic link
View file

@ -0,0 +1 @@
../assets/output/style.css

29
src/ConfigProvider.php Normal file
View file

@ -0,0 +1,29 @@
<?php

namespace App;

use App\LoggingListenerDelegatorFactory;
use Laminas\Stratigility\Middleware\ErrorHandler;

class ConfigProvider
{
/**
* @return array<string, mixed>
*/
public function __invoke() : array
{
return [
'dependencies' => [
'delegators' => [
ErrorHandler::class => [LoggingListenerDelegatorFactory::class],
],
'factories' => [
TestPage::class => TestPageFactory::class,

// Logging Config
'error' => LoggingFactory::class,
]
]
];
}
}

26
src/LoggingFactory.php Normal file
View file

@ -0,0 +1,26 @@
<?php

namespace App;

use Laminas\ServiceManager\ServiceManager;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;

class LoggingFactory
{
public function __invoke(ServiceManager $serviceManager, string $serviceName) : Logger
{
$config = $serviceManager->get('config');
$loggingInfo = $config['logging'];

if (!isset($loggingInfo[$serviceName])) {
throw new \InvalidArgumentException('Unknown service name: ' . $serviceName);
}

$logConfig = $loggingInfo[$serviceName];

$log = new Logger($serviceName);
$log->pushHandler(new StreamHandler($loggingInfo['path'] . $logConfig['file'], $logConfig['level']));
return $log;
}
}

32
src/LoggingListener.php Normal file
View file

@ -0,0 +1,32 @@
<?php

namespace App;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;

class LoggingListener
{
const LOG_FORMAT = '%d [%s] %s: %s';

private LoggerInterface $logger;

public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}

public function __invoke(\Throwable $error, ServerRequestInterface $request, ResponseInterface $response) : void
{
$this->logger->error(
sprintf(
self::LOG_FORMAT,
$response->getStatusCode(),
$request->getMethod(),
(string) $request->getUri(),
(string)$error
)
);
}
}

View file

@ -0,0 +1,20 @@
<?php

namespace App;

use Laminas\Stratigility\Middleware\ErrorHandler;
use Psr\Container\ContainerInterface;

class LoggingListenerDelegatorFactory
{
public function __invoke(ContainerInterface $container, string $name, callable $callback) : ErrorHandler
{
$logger = $container->get('error');

$listener = new LoggingListener($logger);
/** @var ErrorHandler $errorHandler */
$errorHandler = $callback();
$errorHandler->attachListener($listener);
return $errorHandler;
}
}

25
src/TestPage.php Normal file
View file

@ -0,0 +1,25 @@
<?php

namespace App;

use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class TestPage implements RequestHandlerInterface
{
public function __construct(
private TemplateRendererInterface $templateRenderer
)
{
}

public function handle(ServerRequestInterface $request) : ResponseInterface
{
return new HtmlResponse(
$this->templateRenderer->render('app::sample', ['statement' => 'Hello world!'])
);
}
}

15
src/TestPageFactory.php Normal file
View file

@ -0,0 +1,15 @@
<?php

namespace App;

use Laminas\ServiceManager\ServiceManager;
use Mezzio\Template\TemplateRendererInterface;

class TestPageFactory
{
public function __invoke(ServiceManager $serviceManager) : TestPage
{
$templateRenderer = $serviceManager->get(TemplateRendererInterface::class);
return new TestPage($templateRenderer);
}
}

10
tailwind.config.js Normal file
View file

@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./templates/**/*.php"],
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/forms'),
],
}

16
templates/app/sample.php Normal file
View file

@ -0,0 +1,16 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="style.css" rel="stylesheet">
</head>
<body>
<h1 class="text-3xl font-bold underline">
<?php

echo $statement;

?></h1>
</body>
</html>

View file

@ -0,0 +1,8 @@
<?php

declare(strict_types=1);

namespace App\Test\Acceptance;

$container = require 'config/container.php';
return $container;

View file

@ -0,0 +1,5 @@
Feature: Sample feature

Scenario: True is in fact, true.
Given that the world has not ended
Then true still equals true

View file

@ -0,0 +1,25 @@
<?php

namespace AppTest\Acceptance;

use Behat\Behat\Context\Context;
use Webmozart\Assert\Assert;

class SampleContext implements Context
{
/**
* @Given that the world has not ended
*/
public function worldNotEnded() : void
{
Assert::eq(self::class, self::class);
}

/**
* @Then true still equals true
*/
public function sampleTestLine() : void
{
Assert::true(true);
}
}

6
tests/bootstrap.php Normal file
View file

@ -0,0 +1,6 @@
<?php
// turn on all errors
error_reporting(E_ALL);

// autoloader
require ( dirname(__DIR__) . '/vendor/autoload.php');

View file

@ -0,0 +1,13 @@
<?php

namespace AppTest\Functional;

use PHPUnit\Framework\TestCase;

class SampleFunctionalTest extends TestCase
{
public function testSubtraction() : void
{
$this->assertEquals(2, (4-2));
}
}

View file

@ -0,0 +1,13 @@
<?php

namespace AppTest;

use PHPUnit\Framework\TestCase;

class SampleTest extends TestCase
{
public function testTwoPlusTwo() : void
{
$this->assertEquals(4, 2+2);
}
}