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.
-
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/actionconvention, 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
QuinceBasecan 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.
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.
Install Quince using Composer:
composer require quincecontroller/quinceThen 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.
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().
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.
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.
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.
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/quinceIn 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.
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.
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: editorialThis 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.
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.
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 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 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/editThese 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.
Route and alias patterns can contain URL variables:
url: /article/:article_idFor:
/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.
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: browseA 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: trueRouting configuration can therefore describe not only which code should run, but also how it should be invoked.
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/jsonThe 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.
Modules and routing contexts may contain arbitrary metadata:
meta:
section: editorial
template: article.tpl
system: falseQuince 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.
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();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.
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.
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_idwithout needing to know where the application is mounted.
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.
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/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.
Install dependencies:
composer installRun the request-location regression suite:
php tests/RequestLocationResolverTest.phpQuince requires PHP 8 and uses Spyc for YAML parsing.
Spyc is Quince's only external runtime dependency.
GPLv3. See LICENSE.