Skip to content

Latest commit

 

History

53 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Quince

Quince is a lightweight, modular controller, router and dispatcher for PHP.

It is designed around a simple idea: URLs should map flexibly to application code without requiring the structure of the URL to mirror the structure of the code.

Quince applications are composed from self-contained modules discovered from the filesystem. Each module describes its own action class, callable actions, aliases, named routes, routing namespaces, request parameters and metadata in YAML. Quince scans the configured module locations, assembles those definitions into an application-wide routing map, and dispatches incoming requests to the appropriate PHP methods.

Quince provides the controller and routing layer without imposing a full application framework. It does not prescribe models, persistence, templates, dependency injection or frontend architecture.

Features

  • Filesystem-discovered modules Application functionality is organised into self-contained module directories. Adding or removing a module can be as simple as adding or removing its directory.

  • Arbitrary URL mapping Public URLs do not have to resemble controller or method names. Any configured URL can map to any available module action.

  • Conventional routing when useful Quince also supports the simple /module/action convention, allowing rapid development without requiring every URL to be configured explicitly.

  • Named routes Routes can be given stable names and used for reverse URL generation, redirects and links.

  • URL variables Route patterns can extract values directly from URLs and expose them as request parameters.

  • Fixed route parameters Routes can inject additional arbitrary parameters before an action is called, allowing several URLs to invoke the same action with different behaviour.

  • Routing namespaces A single module can expose alternative action classes or behaviour in different Quince namespaces, for example an Ajax or API variant.

  • Module metadata Modules, routes and namespaces can provide application-specific metadata without Quince needing to understand its meaning.

  • Internal forwarding and redirects Action classes extending QuinceBase can forward execution to another action or issue Base-Path-aware redirects.

  • Relocatable applications Quince can run at a web root, in a subdirectory, with rewritten URLs, or using visible index.php/... PATH_INFO routing.

  • Dynamic validation Because application modules are discovered at runtime, Quince verifies that configured classes and callable actions actually exist before dispatching them.

  • Cached module discovery and configuration Module and routing information is cached and rebuilt when installed modules or their YAML definitions change.

  • Minimal dependencies Spyc, used for YAML parsing, is Quince's only external runtime dependency.

Philosophy

Quince deliberately solves a limited set of problems:

What modules are installed?
        ↓
What does this URL mean?
        ↓
Which module should handle it?
        ↓
Which action should run?
        ↓
What parameters and context should it receive?

It leaves the remainder of the application's architecture to the developer.

That makes Quince useful where an application needs strong routing and controller organisation but does not need or want a full-stack framework.

The result is a system intended to be:

modular Functionality is packaged in independently discoverable units.

flexible URLs, module names and PHP method names do not need to mirror one another.

lightweight The controller layer has very few assumptions and very few dependencies.

rapid to develop with A new piece of functionality can be introduced with a module directory, YAML definition and PHP class.

portable Modules carry their own routing information, and applications can move between root, subdirectory, rewritten and PATH_INFO deployments.

Installation

Install Quince using Composer:

composer require quincecontroller/quince

Then include Composer's autoloader in the application's front controller:

require __DIR__.'/vendor/autoload.php';

The Quince engine itself is Composer-autoloaded. Application modules are deliberately separate: they are discovered by Quince from configured module directories and do not need to belong to the package namespace or be registered individually with Composer.

Minimal application and front controller

A minimal Quince application needs a top-level quince.yml, a writable cache directory, one or more module directories, and a front controller such as index.php.

application/
├── index.php
├── quince.yml
├── vendor/
├── var/
│   └── cache/
│       └── quince/
└── Modules/
    └── Articles/
        ├── quince.yml
        └── Articles.class.php

The top-level quince.yml configures the application, including module discovery and the cache location:

quince:
  request_class: QuinceController\QuinceRequest
  exception_handling: throw
  default_module: articles
  cache_dir: var/cache/quince/
  use_checking: true
  use_namespaces: true
  default_content_type: text/html
  default_charset: UTF-8
  base_path: /

  modules:
    config: quince.yml
    storage:
      - Modules/

The corresponding index.php loads Composer, constructs Quince with the application directory and top-level configuration filename, dispatches the current URL, and emits the action result:

<?php

use QuinceController\Quince;

require __DIR__.'/vendor/autoload.php';

// instantiate Quince, for example if the quince.yml config file is in the top level directory (it can be stored anywhere in the tree)
$quince = new Quince(__DIR__.'/', 'quince.yml');

// $request is the current request object including all the logic for handling the request and all the data as to which code is to be executed
$request = $quince->dispatch();

// $result is the data that would be sent to the presentation layer, after your application code has been executed
$result = $request->getResult();

Calling dispatch() resolves the Base Path and request string, discovers or loads the cached module map, resolves aliases and routes, validates the selected class and action, executes the action, and returns the populated QuinceRequest. The action's return value is available through $request->getResult().

Action results and bring()

An action can return one value in the usual PHP way. If that is all it does, getResult() returns that value unchanged:

public function index($get = [], $post = [])
{
    return 'Hello from Quince';
}

For a result with several named values, use bring($name, $value) from an action class:

public function article($get = [], $post = [])
{
    $this->bring('article', $this->loadArticle($get['id']));
    $this->bring('editable', true);
}

In this case getResult() returns a QuinceController\QuinceResult. It implements ArrayAccess, IteratorAggregate, Countable, and JsonSerializable, and also provides toArray().

An action may use both styles. Its returned value is then available at the reserved _returned offset. If the return value is an array, its entries are also merged into the array-like result; explicitly brought names take precedence if the same name occurs in both places. Application code cannot bring, replace, or unset _returned.

$result = $request->getResult();

echo $result['article'];
echo $result['_returned'];

When an action returns a string, integer, float, or true as well as bringing data, casting its QuinceResult to a string produces that scalar return value. Structured returns are not implicitly rendered; select a named value, call toArray(), or encode the result explicitly for the required presentation format.

For the explicit return style familiar from Laravel and Symfony, action classes which extend QuinceBase can use its protected result() method. No additional import is required:

public function article($get = [], $post = [])
{
    return $this->result([
        'article' => $this->loadArticle($get['id']),
        'editable' => true,
    ]);
}

Quince also provides the same operation as a namespaced function. This is useful outside a QuinceBase action, or when a functional style is preferred. PHP function imports apply per file, so this form requires an explicit import:

use function QuinceController\result;

return result([
    'article' => $article,
    'editable' => true,
]);

Both forms are shorthand for constructing new QuinceResult([...]). Quince does not define a global result() function because that generic name could conflict with another package. Because the value is already an intentional result object, Quince passes it through unchanged and does not add _returned. If an action uses both bring() and either form of result(), the two collections are merged; values in the explicitly returned result win name collisions.

Modules

A typical application might look like:

application/
├── quince.yml
├── cache/
└── Modules/
    ├── Articles/
    │   ├── quince.yml
    │   └── Articles.class.php
    │
    └── Users/
        ├── quince.yml
        ├── Users.class.php
        └── UsersAjax.class.php

The application-level configuration tells Quince where its modules are stored.

Quince scans those locations for module directories containing the configured module definition file, normally quince.yml.

Each discovered module contributes its own configuration to the application-wide routing map.

Module discovery and caching

Quince discovers its application structure from the configured module locations.

Conceptually:

Module directory A ─┐
Module directory B ─┤
Module directory C ─┼──► Quince ───► application routing map
Module directory D ─┘

The installed module set and module configuration are cached.

When modules are added or removed, or their configuration changes, Quince rebuilds the affected cached routing information.

This allows filesystem-based application composition without requiring every request to perform a complete module scan and YAML parse.

Cache directory requirements

Every Quince application must choose a cache location and set it using cache_dir in its top-level quince.yml:

quince:
  cache_dir: var/cache/quince/

The path is resolved relative to the application home directory supplied to the Quince constructor. For example:

$quince = new Quince(__DIR__.'/', 'quince.yml');

with cache_dir: var/cache/quince/ uses:

<application-directory>/var/cache/quince/

The directory must already exist and must be writable by the PHP process. Quince does not currently create it automatically. For local development it can be created with:

mkdir -p var/cache/quince
chmod u+rwx var/cache/quince

In production, ensure that the PHP-FPM or web-server user owns the directory or has appropriate write permission. Avoid making it world-writable. A cache directory outside the publicly served document root is recommended.

Application module loading

While Composer manages the Quince engine itself, Quince manages application modules.

The engine uses PSR-4 classes under:

QuinceController\

Application module classes remain dynamically discovered files. They do not need to belong to the package namespace or be Composer-autoloaded.

A module therefore remains straightforward:

use QuinceController\QuinceBase;

class Articles extends QuinceBase
{
    public function index($get = array(), $post = array())
    {
        return 'Hello from Quince';
    }
}

This separation preserves one of Quince's central properties: application functionality can be added, removed or moved as modules rather than having to be registered in a central application class map.

Creating a module

A module action class normally extends QuinceBase:

use QuinceController\QuinceBase;

class Articles extends QuinceBase
{
    public function index($get = array(), $post = array())
    {
        return 'Latest articles';
    }

    public function view($get = array(), $post = array())
    {
        $id = $this->getRequest()->getRequestParameter('article_id');

        return 'Viewing article '.$id;
    }
}

This base class provides an API for accessing URL variables, forwarding, redirection.

A corresponding module definition might be:

module:
  class: Articles
  shortname: articles
  identifier: org.example.Articles
  longname: Articles
  default_action: index

  aliases:
    - {url: /news, action: index}
    - {url: /article/:article_id, action: view}

  routes:
    home:
      action: index
      url: /news

    article:
      action: view
      url: /article/:article_id

  meta:
    section: editorial

This module can now be reached in several ways.

The conventional URL:

/articles/index

calls:

Articles::index()

The configured alias:

/news

calls the same method without exposing either the module or action name.

And:

/article/42

calls:

Articles::view()

with:

$this->getRequest()->getRequestParameter('article_id');

returning:

42

The relationship is therefore:

public URL
    ↓
route or alias
    ↓
module
    ↓
action
    ↓
PHP method

The URL and the PHP structure remain related, but neither is required to mirror the other.

Provided example module: Articles

examples/Modules/Articles contains a complete discovered module demonstrating:

  • module discovery;
  • callable actions;
  • aliases;
  • named routes;
  • URL variables;
  • fixed route parameters;
  • metadata;
  • an alternative Ajax routing namespace and action class.

It is intended both as a runnable example and as a starting point for new modules.

Actions

An action is simply a callable public method of the configured module class.

For example:

class Articles extends QuinceBase
{
    public function index()
    {
        // ...
    }

    public function search()
    {
        // ...
    }

    public function archive()
    {
        // ...
    }
}

With conventional routing enabled:

/articles/search

maps naturally to:

Articles::search()

Explicit aliases and routes can map completely different URLs to the same action.

Because modules are dynamically discovered, Quince checks that their configured classes exist and that selected actions are callable before executing them.

Aliases

Aliases provide arbitrary inbound URL mappings.

aliases:
  - {url: /news, action: index}
  - {url: /latest, action: latest}
  - {url: /story/:story_id, action: view}

The URL:

/story/125

can therefore invoke:

Articles::view()

with story_id supplied as a request parameter.

Aliases are useful where the public URL structure should remain independent from module and method names.

Named routes

Named routes provide the same flexible mapping while also giving a URL a stable logical name:

routes:
  home:
    action: index
    url: /news

  article:
    action: view
    url: /article/:article_id

  edit_article:
    action: edit
    url: /article/:article_id/edit

These routes can be referred to by name rather than by hard-coded URL.

For example:

@articles:article

identifies the article route belonging to the articles module.

Named routes can be used when generating URLs and redirects, allowing the public URL to change without requiring corresponding changes throughout application code.

URL variables

Route and alias patterns can contain URL variables:

url: /article/:article_id

For:

/article/42

Quince extracts:

article_id = 42

and places it into the current request.

Quince historically supports both :name and $name forms in route patterns.

Fixed request parameters

Routes can also provide parameters that do not appear in the URL.

For example:

routes:
  browse_model:
    action: browse
    url: /items/:type
    params:
      use_type: true
      mode: browse

A request for:

/items/articles

can therefore produce:

type     = articles
use_type = true
mode     = browse

before invoking the configured action.

This allows several routes to share an action while supplying different contextual behaviour:

routes:
  published:
    action: browse
    url: /articles/published
    params:
      status: published

  drafts:
    action: browse
    url: /articles/drafts
    params:
      status: draft

  featured:
    action: browse
    url: /articles/featured
    params:
      featured: true

Routing configuration can therefore describe not only which code should run, but also how it should be invoked.

Quince namespaces

Quince namespaces allow one logical module to provide alternative implementations for different contexts.

They are distinct from PHP namespaces.

For example:

module:
  class: Articles

  namespaces:
    ajax:
      class: ArticlesAjax
      meta:
        template: _blank.tpl

    api:
      class: ArticlesApi
      content_type: application/json

The module remains logically the same module, while Quince can select an alternative action class or configuration according to the namespace being used.

Namespaces can provide alternatives including:

  • action class;
  • default action;
  • action prefix;
  • content type;
  • character set;
  • metadata.

This is useful for providing, for example, normal HTML, Ajax and API interfaces to the same logical area of an application without turning them into unrelated modules.

Metadata

Modules and routing contexts may contain arbitrary metadata:

meta:
  section: editorial
  template: article.tpl
  system: false

Quince carries this metadata on the request:

$request->getMeta('section');
$request->hasMeta('template');
$request->getMetas();

Quince does not assign semantics to application metadata. It simply provides a convenient way for contextual information to travel with a module, route or namespace.

Request parameters

The current request collects parameters from several sources:

  • values extracted from route URLs;
  • fixed route parameters;
  • GET variables;
  • POST variables.

They can be inspected using:

$request->hasRequestParameter('article_id');

$id = $request->getRequestParameter('article_id');

$page = $request->getRequestParameter('page', 1);

$params = $request->getRequestParameters();

Forwarding

A module action can transfer execution directly to another module and action without the need for a further request/redirect:

$this->forward('users', 'login');

A forward takes place internally within the current request. No HTTP redirect is sent to the client.

Quince limits repeated forwarding to prevent accidental forwarding loops.

Redirects

Action classes extending QuinceBase can redirect:

$this->redirect('/login');

or redirect using named routes.

Redirects take the resolved Base Path into account, allowing the same module code to work whether the application is mounted at / or below a subdirectory.

Base Path

The Base Path is the public URL prefix at which the Quince application is mounted.

For an application at:

https://example.org/

the Base Path is:

/

For an application at:

https://example.org/tools/app/

the Base Path is:

/tools/app/

It is normalised internally to either / or a leading-and-trailing-slash form.

An explicit Base Path can be configured:

quince:
  base_path: /tools/app/

Quince can also resolve its Base Path automatically from the available request environment.

Routing then operates against the application-relative part of the request:

Request URI:
    /tools/app/article/42

Base Path:
    /tools/app/

Quince request:
    article/42

This means route definitions remain portable:

url: /article/:article_id

without needing to know where the application is mounted.

Rewritten and PATH_INFO URLs

Quince supports both rewritten URLs:

/article/42

and visible front-controller or PATH_INFO URLs:

/index.php/article/42

The same applies to applications mounted in a subdirectory:

/tools/app/article/42

or:

/tools/app/index.php/article/42

Quince normalises these forms so that each can resolve to the same internal request:

article/42

When PATH_INFO routing is used, the visible front controller is retained as part of the public Base Path used when generating links and redirects.

For example:

/tools/app/index.php/

may be used as the effective public prefix while Quince separately records index.php as the front controller.

Base Path compatibility

Earlier versions of Quince referred to the Base Path as the application's domain.

That terminology is deprecated because the value represents a URL path rather than a hostname.

The old configuration key remains supported:

quince:
  domain: /tools/app/

and the legacy methods remain compatibility aliases:

$request->getDomain();
$request->setDomain($path);

New applications should use:

$request->getBasePath();
$request->setBasePath($path);

and:

quince:
  base_path: /tools/app/

Lifecycle hooks

QuinceBase also provides lifecycle hooks around action execution:

protected function __moduleConstruct()
{
    // Called when the module action object is constructed.
}

public function __pre()
{
    // Called immediately before the selected action.
}

public function __post()
{
    // Called immediately after the selected action.
}

These can provide module-wide initialisation or behaviour shared across actions.

Development

Install dependencies:

composer install

Run the request-location regression suite:

php tests/RequestLocationResolverTest.php

Dependencies

Quince requires PHP 8 and uses Spyc for YAML parsing.

Spyc is Quince's only external runtime dependency.

Licence

GPLv3. See LICENSE.

About

Quince - A lightweight modular PHP controller layer

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages