Documentation

Requirements

Zack! requirements are:

  • PHP: 8.2 / 8.3 / 8.4
  • Composer: 2.x

Composer --no-dev requirements are:

  • symfony/dependency-injection: ^7.2
  • symfony/event-dispatcher: ^7.2
  • symfony/finder: ^7.2
  • symfony/http-foundation: ^7.2
  • symfony/http-kernel: ^7.2
  • symfony/routing: ^7.2
  • twig/markdown-extra: ^3.21
  • twig/twig: ^3.20

Installation

Create a new project folder and change into it.

mkdir myproject
cd myproject

Install Zack! using Composer:

composer require tebe/zack:dev-main

In your myproject folder add the following folders and files:

myproject/
├─ routes/
│  └─ index.get.html
└─ web/
   └─ index.php

Add the following content to the files:

routes/index.get.html

<h1>Hello World!</h1>

web/index.php

<?php

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

$config = [
    'basePath' => dirname(__DIR__),
];

(new tebe\zack\Zack($config))->run();

Start PHP's built-in web server:

cd myproject
php -S localhost:8888 -t web

Open http://localhost:8888 with your preferred web browser.

Folder Structure

A typical project folder structure looks like the following:

project/                # Project root folder on your server
├─ cache/               # Cached files
├─ config/              # Config files
├─ logs/                # Log files
├─ routes/              # Routes for your website
│  └─ index.get.html    # The only route in this example
├─ vendor/              # Composer dependencies
├─ views/               # Twig templates
│  ├─ base.html.twig    # Twig base layout file
│  └─ error.html.twig   # Twig file for displaying errors
└─ web/                 # Web server public folder
   ├─ assets/           # Asset files like css or js
   └─ index.php         # Website bootstrap file

Normally you only work in the routes and views folders.

File-Based Routing

Zack! is using file-based routing for your routes. Files are automatically mapped to Symfony routes. Defining a route is as simple as creating a file inside the routes directory.

You can only define one handler per files and you can append the HTTP method to the filename to define a specific request method. If no method is specified, the route applies to all methods.

routes/
├─ api/
│  └─ test.patch.php   # PATCH /api/test
├─ index.php           # ANY   /
├─ contact.get.php     # GET   /contact
└─ contact.post.php    # POST  /contact

You can nest routes by creating subdirectories.

routes/
├─ communities/
│  ├─ index.get.php
│  ├─ index.post.php
│  └─ [id]/
│     ├─ index.get.php
│     └─ index.post.php
├─ hello.get.php
└─ hello.post.php

Simple Routes

First, create a file in routes directory. The filename will be the route path.

Then, create a file that returns a JSON response. This file will be executed when the route is matched.

#routes/api/ping.php

<?php

use Symfony\Component\HttpFoundation\Response;

return new Response('{"ping": "pong"}', 200, [
    'Content-Type' => 'application/json; charset=UTF-8',
]);

Route With Params

Single Param

To define a route with params, use the [<param>] syntax where <param> is the name of the param. The param will be available in the $request->attributes object.

#routes/hello/[name].php

<?php

use Symfony\Component\HttpFoundation\Response;

$name = $request->attributes->get('name');

return new Response('Hello ' . $name . '!', 200);

Call the route with the param /hello/zack, you will get:

#Response

Hello zack!
Multiple Params

You can define multiple params in a route using [<param1>]/[<param2>] syntax where each param is a folder. You cannot define multiple params in a single filename of folder.

#routes/hello/[name]/[age].php

<?php

use Symfony\Component\HttpFoundation\Response;

$name = $request->attributes->get('name');
$age = $request->attributes->get('age');

return new Response("Hello $name! You are $age years old.", 200);
Catch All Params

You can capture all the remaining parts of a URL using [...<param>] syntax. This will include the / in the param.

#routes/hello/[...name].php

<?php

use Symfony\Component\HttpFoundation\Response;

$name = $request->attributes->get('name');

return new Response("Hello $name!", 200);

Call the route with the param /hello/zack/is/nice, you will get:

#Response

Hello zack/is/nice!

Specific Request Method

You can append the HTTP method to the filename to force the route to be matched only for a specific HTTP request method. For example hello.get.php will only match for GET requests. You can use any HTTP method you want.

Example with POST method.

# routes/users.post.php

<?php

use Symfony\Component\HttpFoundation\Response;

// Do something with body like saving it to a database

return new Response('{"updated": true}', 200, [
    'Content-Type' => 'application/json; charset=UTF-8',
]);

Catch All Route

You can create a special route that will match all routes that are not matched by any other route. This is useful for creating a default route.

To create a catch all route, create a file named [...].php in the routes directory.

#routes/[...].php

<?php

use Symfony\Component\HttpFoundation\Response;

$path = $request->attributes->get('path');

return new Response("Hello $path!", 200);

Route Handler

You can use the file extension of a route file to force the route to be handled by a specific route handler.

routes/
├─ htm-page.htm
├─ html-page.html
├─ json-page.json
├─ md-page.md
├─ markdown-page.markdown
├─ text-page.txt
└─ php-page.php

Zack! is currently delivered with the following route handlers:

HTML Route Handler

File extensions: htm, html
Response content-type: text/html; charset=UTF-8

The content of the HTML file is taken. The Twig layout is determined via the layout comment <!-- layout: my-layout.html.twig --> in the HTML content. The page title is determined by the H1-H3 headings in the HTML content. The layout is applied and output together with the page title and the HTML content.

Markdown Route Handler

File extensions: markdown, md
Response content-type: text/html; charset=UTF-8

The content of the Markdown file is taken. The markdown is converted to HTML using one of the following Composer packages:

  • league/commonmark
  • michelf/php-markdown
  • erusev/parsedown

The Twig layout is determined via the layout comment <!-- layout: my-layout.html.twig --> in the HTML content. The page title is determined by the H1-H3 headings in the HTML content. The layout is applied and output together with the page title and the HTML content.

PHP Route Handler

File extension: php
Response content-type: text/html; charset=UTF-8, application/json; charset=UTF-8, or other

The content-type of the response can be set explicitly in a PHP route handler.

Echoing Content

The echoed content of the PHP file is taken.

If the HTML content contains an html element or a Doctype, the HTML content is output as it is.

Otherwise the Twig layout is determined via the layout comment <!-- layout: my-layout.html.twig --> in the HTML content. The page title is determined by the H1-H3 headings in the HTML content. The layout is applied and output together with the page title and the HTML content.

Returning Response

If you want finer control over the HTTP response, you can return a string, an array or a Symfony\Component\HttpFoundation\Response object.

If the return value is a string, it is output as HTML with the content type text/html; charset=UTF-8. The same logic is applied as for echoing content.

If the return value is an array, it is JSON encoded and output with the content-type application/json; charset=UTF-8.

If the return value is a Symfony\Component\HttpFoundation\Response object, it is output unchanged together with the underlying content type. With returning a response object you will have full control over the HTTP response. There are several response subclasses to help you return JSON, redirect, stream file downloads and more.

Generic Route Handler

The generic route handler is a handler that supports the following file types:

---------------------------------------------------
file extension      content type
---------------------------------------------------
json                application/json; charset=UTF-8
txt                 text/plain; charset=UTF-8
xml                 application/xml; charset=UTF-8
---------------------------------------------------

The contents of the file are read and output together with the corresponding content type from the above mapping.

Configuration

TBD

Events

Zack! Events

Zack! ships with the following events:

  • zack.container: This event is dispatched after the container has been built.
  • zack.controller: This event is dispatched just before the controller (i.e. the route handler) is determined.
  • zack.routes: This event is dispatched after the routes have been built.

HttpKernel Events

Zack! supports the following Symfony HttpKernel events:

  • kernel.controller: This event is dispatched very early, before the controller is determined.
  • kernel.controller_arguments: This event is dispatched after the controller has been resolved but before executing it.
  • kernel.view: This event is dispatched just before a controller is called.
  • kernel.response: This event is dispatched after the controller or any kernel.view listener returns a Response object.
  • kernel.finish_request: This event is dispatched after the kernel.response event.
  • kernel.terminate: This event is dispatched after the response has been sent (after the execution of the handle() method).
  • kernel.exception: This event is dispatched as soon as an error occurs during the handling of the HTTP request.

Read Built-in Symfony Events for more information.

Development Environment

Create Docker Image

Create Docker image based on the latest supported PHP version

docker build -t zack https://github.com/tbreuss/zack.git

Optionally you can also use an older PHP version

docker build --build-arg PHP_VERSION=8.2 -t zack https://github.com/tbreuss/zack.git
docker build --build-arg PHP_VERSION=8.3 -t zack https://github.com/tbreuss/zack.git

Run Website

Clone project

git clone https://github.com/tbreuss/zack.git

Change directory

cd zack

Install packages

docker run --rm -it -v .:/app zack composer install

Run website

docker run --rm -v .:/app -p 8888:8888 zack php -S 0.0.0.0:8888 -t /app/website/web

Debug website using Xdebug

docker run --rm -e XDEBUG_CONFIG="client_host=172.17.0.1" -e XDEBUG_MODE=debug -e XDEBUG_SESSION_START=true -v .:/app -p 8888:8888 zack php -S 0.0.0.0:8888 -t /app/website/web

Code Coverage

Start website using Xdebug in coverage mode using phpunit/php-code-coverage

docker run --rm -e XDEBUG_MODE=coverage -p 8888:8888 -v .:/app zack php -S 0.0.0.0:8888 -t /app/website/web

Create HTML report using phpcov

docker run --rm -it -v .:/app zack php vendor/bin/phpcov merge --html /app/.coverage/report /app/.coverage/files

Open generated HTML report in browser

docker run --rm -p 8888:8888 -v .:/app     zack php -S 0.0.0.0:8888 -t /app/.coverage/report

Testing

Coding Style

Fix coding style issues using PHP-CS-Fixer

./bin/fix-coding-style.sh

Static Code Analysis

Analyse code using PHPStan

./bin/analyse-code.sh

Functional Tests

Run functional tests using Hurl

./bin/test-code.sh localhost:9330

Website Tests

Run website tests using Hurl

./bin/test-website.sh localhost:9331