Usage¶
Installation¶
If you want to get started fast, use the PrestoPHP Skeleton:
composer create-project prestophp/prestophp-skeleton path/to/install
If you want more flexibility, use Composer instead:
composer require PrestoPHP/PrestoPHP:~2.0
Web Server¶
All examples in the documentation rely on a well-configured web server; read the webserver documentation to check yours.
Bootstrap¶
To bootstrap PrestoPHP, all you need to do is require the vendor/autoload.php
file and create an instance of PrestoPHP\Application
. After your controller
definitions, call the run
method on your application:
// web/index.php
require_once __DIR__.'/../vendor/autoload.php';
$app = new PrestoPHP\Application();
// ... definitions
$app->run();
Tip
When developing a website, you might want to turn on the debug mode to ease debugging:
$app['debug'] = true;
Tip
If your application is hosted behind a reverse proxy at address $ip
,
and you want PrestoPHP to trust the X-Forwarded-For*
headers, you will
need to run your application like this:
use Symfony\Component\HttpFoundation\Request;
Request::setTrustedProxies(array($ip));
$app->run();
Routing¶
In PrestoPHP you define a route and the controller that is called when that route is matched. A route pattern consists of:
Pattern: The route pattern defines a path that points to a resource. The pattern can include variable parts and you are able to set RegExp requirements for them.
Method: One of the following HTTP methods:
GET
,POST
,PUT
,DELETE
,PATCH
, orOPTIONS
. This describes the interaction with the resource.
The controller is defined using a closure like this:
function () {
// ... do something
}
The return value of the closure becomes the content of the page.
Example GET Route¶
Here is an example definition of a GET
route:
$blogPosts = array(
1 => array(
'date' => '2011-03-29',
'author' => 'igorw',
'title' => 'Using PrestoPHP',
'body' => '...',
),
);
$app->get('/blog', function () use ($blogPosts) {
$output = '';
foreach ($blogPosts as $post) {
$output .= $post['title'];
$output .= '<br />';
}
return $output;
});
Visiting /blog
will return a list of blog post titles. The use
statement means something different in this context. It tells the closure to
import the $blogPosts
variable from the outer scope. This allows you to use
it from within the closure.
Dynamic Routing¶
Now, you can create another controller for viewing individual blog posts:
$app->get('/blog/{id}', function (PrestoPHP\Application $app, $id) use ($blogPosts) {
if (!isset($blogPosts[$id])) {
$app->abort(404, "Post $id does not exist.");
}
$post = $blogPosts[$id];
return "<h1>{$post['title']}</h1>".
"<p>{$post['body']}</p>";
});
This route definition has a variable {id}
part which is passed to the
closure.
The current Application
is automatically injected by PrestoPHP to the Closure
thanks to the type hinting.
When the post does not exist, you are using abort()
to stop the request
early. It actually throws an exception, which you will see how to handle later
on.
Example POST Route¶
POST routes signify the creation of a resource. An example for this is a
feedback form. You will use the mail
function to send an e-mail:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$app->post('/feedback', function (Request $request) {
$message = $request->get('message');
mail('feedback@yoursite.com', '[YourSite] Feedback', $message);
return new Response('Thank you for your feedback!', 201);
});
It is pretty straightforward.
Note
There is a SwiftmailerServiceProvider
included that you can use instead of mail()
.
The current request
is automatically injected by PrestoPHP to the Closure
thanks to the type hinting. It is an instance of
Request, so you can fetch variables using the request get
method.
Instead of returning a string you are returning an instance of Response.
This allows setting an HTTP status code, in this case it is set to
201 Created
.
Note
PrestoPHP always uses a Response
internally, it converts strings to
responses with status code 200
.
Other methods¶
You can create controllers for most HTTP methods. Just call one of these
methods on your application: get
, post
, put
, delete
, patch
, options
:
$app->put('/blog/{id}', function ($id) {
// ...
});
$app->delete('/blog/{id}', function ($id) {
// ...
});
$app->patch('/blog/{id}', function ($id) {
// ...
});
Tip
Forms in most web browsers do not directly support the use of other HTTP
methods. To use methods other than GET and POST you can utilize a special
form field with a name of _method
. The form’s method
attribute must
be set to POST when using this field:
<form action="/my/target/route/" method="post">
<!-- ... -->
<input type="hidden" id="_method" name="_method" value="PUT" />
</form>
You need to explicitly enable this method override:
use Symfony\Component\HttpFoundation\Request;
Request::enableHttpMethodParameterOverride();
$app->run();
You can also call match
, which will match all methods. This can be
restricted via the method
method:
$app->match('/blog', function () {
// ...
});
$app->match('/blog', function () {
// ...
})
->method('PATCH');
$app->match('/blog', function () {
// ...
})
->method('PUT|POST');
Note
The order in which the routes are defined is significant. The first matching route will be used, so place more generic routes at the bottom.
Route Variables¶
As it has been shown before you can define variable parts in a route like this:
$app->get('/blog/{id}', function ($id) {
// ...
});
It is also possible to have more than one variable part, just make sure the closure arguments match the names of the variable parts:
$app->get('/blog/{postId}/{commentId}', function ($postId, $commentId) {
// ...
});
While it’s not recommended, you could also do this (note the switched arguments):
$app->get('/blog/{postId}/{commentId}', function ($commentId, $postId) {
// ...
});
You can also ask for the current Request and Application objects:
$app->get('/blog/{id}', function (Application $app, Request $request, $id) {
// ...
});
Note
Note for the Application and Request objects, PrestoPHP does the injection based on the type hinting and not on the variable name:
$app->get('/blog/{id}', function (Application $foo, Request $bar, $id) {
// ...
});
Route Variable Converters¶
Before injecting the route variables into the controller, you can apply some converters:
$app->get('/user/{id}', function ($id) {
// ...
})->convert('id', function ($id) { return (int) $id; });
This is useful when you want to convert route variables to objects as it allows to reuse the conversion code across different controllers:
$userProvider = function ($id) {
return new User($id);
};
$app->get('/user/{user}', function (User $user) {
// ...
})->convert('user', $userProvider);
$app->get('/user/{user}/edit', function (User $user) {
// ...
})->convert('user', $userProvider);
The converter callback also receives the Request
as its second argument:
$callback = function ($post, Request $request) {
return new Post($request->attributes->get('slug'));
};
$app->get('/blog/{id}/{slug}', function (Post $post) {
// ...
})->convert('post', $callback);
A converter can also be defined as a service. For example, here is a user converter based on Doctrine ObjectManager:
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class UserConverter
{
private $om;
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
public function convert($id)
{
if (null === $user = $this->om->find('User', (int) $id)) {
throw new NotFoundHttpException(sprintf('User %d does not exist', $id));
}
return $user;
}
}
The service will now be registered in the application, and the
convert()
method will be used as converter (using the syntax
service_name:method_name
):
$app['converter.user'] = function () {
return new UserConverter();
};
$app->get('/user/{user}', function (User $user) {
// ...
})->convert('user', 'converter.user:convert');
Requirements¶
In some cases you may want to only match certain expressions. You can define
requirements using regular expressions by calling assert
on the
Controller
object, which is returned by the routing methods.
The following will make sure the id
argument is a positive integer, since
\d+
matches any amount of digits:
$app->get('/blog/{id}', function ($id) {
// ...
})
->assert('id', '\d+');
You can also chain these calls:
$app->get('/blog/{postId}/{commentId}', function ($postId, $commentId) {
// ...
})
->assert('postId', '\d+')
->assert('commentId', '\d+');
Conditions¶
Besides restricting route matching based on the HTTP method or parameter
requirements, you can set conditions on any part of the request by calling
when
on the Controller
object, which is returned by the routing
methods:
$app->get('/blog/{id}', function ($id) {
// ...
})
->when("request.headers.get('User-Agent') matches '/firefox/i'");
The when
argument is a Symfony Expression , which means that you need to
add symfony/expression-language
as a dependency of your project.
Default Values¶
You can define a default value for any route variable by calling value
on
the Controller
object:
$app->get('/{pageName}', function ($pageName) {
// ...
})
->value('pageName', 'index');
This will allow matching /
, in which case the pageName
variable will
have the value index
.
Named Routes¶
Some providers can make use of named routes. By default PrestoPHP will generate an
internal route name for you but you can give an explicit route name by calling
bind
:
$app->get('/', function () {
// ...
})
->bind('homepage');
$app->get('/blog/{id}', function ($id) {
// ...
})
->bind('blog_post');
Controllers as Classes¶
Instead of anonymous functions, you can also define your controllers as
methods. By using the ControllerClass::methodName
syntax, you can tell
PrestoPHP to lazily create the controller object for you:
$app->get('/', 'Acme\\Foo::bar');
use PrestoPHP\Application;
use Symfony\Component\HttpFoundation\Request;
namespace Acme
{
class Foo
{
public function bar(Request $request, Application $app)
{
// ...
}
}
}
This will load the Acme\Foo
class on demand, create an instance and call
the bar
method to get the response. You can use Request
and
PrestoPHP\Application
type hints to get $request
and $app
injected.
It is also possible to define your controllers as services.
Global Configuration¶
If a controller setting must be applied to all controllers (a converter, a
middleware, a requirement, or a default value), configure it on
$app['controllers']
, which holds all application controllers:
$app['controllers']
->value('id', '1')
->assert('id', '\d+')
->requireHttps()
->method('get')
->convert('id', function () { /* ... */ })
->before(function () { /* ... */ })
->when('request.isSecure() == true')
;
These settings are applied to already registered controllers and they become the defaults for new controllers.
Note
The global configuration does not apply to controller providers you might mount as they have their own global configuration (read the dedicated chapter for more information).
Error Handlers¶
When an exception is thrown, error handlers allow you to display a custom error page to the user. They can also be used to do additional things, such as logging.
To register an error handler, pass a closure to the error
method which
takes an Exception
argument and returns a response:
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
$app->error(function (\Exception $e, Request $request, $code) {
return new Response('We are sorry, but something went terribly wrong.');
});
You can also check for specific errors by using the $code
argument, and
handle them differently:
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
$app->error(function (\Exception $e, Request $request, $code) {
switch ($code) {
case 404:
$message = 'The requested page could not be found.';
break;
default:
$message = 'We are sorry, but something went terribly wrong.';
}
return new Response($message);
});
You can restrict an error handler to only handle some Exception classes by setting a more specific type hint for the Closure argument:
use Symfony\Component\HttpFoundation\Request;
$app->error(function (\LogicException $e, Request $request, $code) {
// this handler will only handle \LogicException exceptions
// and exceptions that extend \LogicException
});
Note
As PrestoPHP ensures that the Response status code is set to the most appropriate one depending on the exception, setting the status on the response alone won’t work.
If you want to overwrite the status code, which you should not without a
good reason, set the X-Status-Code
header (on Symfony until version
3.2):
return new Response('Error', 404 /* ignored */, array('X-Status-Code' => 200));
As of Symfony 5.0, call
ExceptionEvent::allowCustomResponseCode()
first and then
then set the status code on the response as normal. The kernel will now use
your status code when sending the response to the client. The
ExceptionEvent
is passed to the error callback as a 4th
parameter:
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
$app->error(function (\Exception $e, Request $request, $code, ExceptionEvent $event) {
$event->allowCustomResponseCode();
$response = new Response('No Content', 204);
return $response;
});
If you want to use a separate error handler for logging, make sure you register it with a higher priority than response error handlers, because once a response is returned, the following handlers are ignored.
Note
PrestoPHP ships with a provider for Monolog which handles logging of errors. Check out the Providers chapter for details.
Tip
PrestoPHP comes with a default error handler that displays a detailed error
message with the stack trace when debug is true, and a simple error
message otherwise. Error handlers registered via the error()
method
always take precedence but you can keep the nice error messages when debug
is turned on like this:
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
$app->error(function (\Exception $e, Request $request, $code) use ($app) {
if ($app['debug']) {
return;
}
// ... logic to handle the error and return a Response
});
The error handlers are also called when you use abort
to abort a request
early:
$app->get('/blog/{id}', function (PrestoPHP\Application $app, $id) use ($blogPosts) {
if (!isset($blogPosts[$id])) {
$app->abort(404, "Post $id does not exist.");
}
return new Response(...);
});
You can convert errors to Exceptions
, check out the cookbook chapter for details.
View Handlers¶
View Handlers allow you to intercept a controller result that is not a
Response
and transform it before it gets returned to the kernel.
To register a view handler, pass a callable (or string that can be resolved to a
callable) to the view()
method. The callable should accept some sort of result
from the controller:
$app->view(function (array $controllerResult) use ($app) {
return $app->json($controllerResult);
});
View Handlers also receive the Request
as their second argument,
making them a good candidate for basic content negotiation:
$app->view(function (array $controllerResult, Request $request) use ($app) {
$acceptHeader = $request->headers->get('Accept');
$bestFormat = $app['negotiator']->getBestFormat($acceptHeader, array('json', 'xml'));
if ('json' === $bestFormat) {
return new JsonResponse($controllerResult);
}
if ('xml' === $bestFormat) {
return $app['serializer.xml']->renderResponse($controllerResult);
}
return $controllerResult;
});
View Handlers will be examined in the order they are added to the application and PrestoPHP will use type hints to determine if a view handler should be used for the current result, continuously using the return value of the last view handler as the input for the next.
Note
You must ensure that PrestoPHP receives a Response
or a string as the result of
the last view handler (or controller) to be run.
Redirects¶
You can redirect to another page by returning a RedirectResponse
response,
which you can create by calling the redirect
method:
$app->get('/', function () use ($app) {
return $app->redirect('/hello');
});
This will redirect from /
to /hello
.
Forwards¶
When you want to delegate the rendering to another controller, without a round-trip to the browser (as for a redirect), use an internal sub-request:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
$app->get('/', function () use ($app) {
// forward to /hello
$subRequest = Request::create('/hello', 'GET');
return $app->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
});
Tip
You can also generate the URI via the built-in URL generator:
$request = Request::create($app['url_generator']->generate('hello'), 'GET');
There’s some more things that you need to keep in mind though. In most cases you will want to forward some parts of the current master request to the sub-request. That includes: Cookies, server information, session. Read more on how to make sub-requests.
JSON¶
If you want to return JSON data, you can use the json
helper method.
Simply pass it your data, status code and headers, and it will create a JSON
response for you:
$app->get('/users/{id}', function ($id) use ($app) {
$user = getUser($id);
if (!$user) {
$error = array('message' => 'The user was not found.');
return $app->json($error, 404);
}
return $app->json($user);
});
Streaming¶
It’s possible to stream a response, which is important in cases when you don’t want to buffer the data being sent:
$app->get('/images/{file}', function ($file) use ($app) {
if (!file_exists(__DIR__.'/images/'.$file)) {
return $app->abort(404, 'The image was not found.');
}
$stream = function () use ($file) {
readfile($file);
};
return $app->stream($stream, 200, array('Content-Type' => 'image/png'));
});
If you need to send chunks, make sure you call ob_flush
and flush
after every chunk:
$stream = function () {
$fh = fopen('http://www.example.com/', 'rb');
while (!feof($fh)) {
echo fread($fh, 1024);
ob_flush();
flush();
}
fclose($fh);
};
Sending a file¶
If you want to return a file, you can use the sendFile
helper method.
It eases returning files that would otherwise not be publicly available. Simply
pass it your file path, status code, headers and the content disposition and it
will create a BinaryFileResponse
response for you:
$app->get('/files/{path}', function ($path) use ($app) {
if (!file_exists('/base/path/' . $path)) {
$app->abort(404);
}
return $app->sendFile('/base/path/' . $path);
});
To further customize the response before returning it, check the API doc for SymfonyComponentHttpFoundationBinaryFileResponse:
return $app
->sendFile('/base/path/' . $path)
->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'pic.jpg')
;
Traits¶
PrestoPHP comes with PHP traits that define shortcut methods.
Almost all built-in service providers have some corresponding PHP traits. To use them, define your own Application class and include the traits you want:
use PrestoPHP\Application;
class MyApplication extends Application
{
use Application\TwigTrait;
use Application\SecurityTrait;
use Application\FormTrait;
use Application\UrlGeneratorTrait;
use Application\SwiftmailerTrait;
use Application\MonologTrait;
use Application\TranslationTrait;
}
You can also define your own Route class and use some traits:
use PrestoPHP\Route;
class MyRoute extends Route
{
use Route\SecurityTrait;
}
To use your newly defined route, override the $app['route_class']
setting:
$app['route_class'] = 'MyRoute';
Read each provider chapter to learn more about the added methods.
Security¶
Make sure to protect your application against attacks.
Escaping¶
When outputting any user input, make sure to escape it correctly to prevent Cross-Site-Scripting attacks.
Escaping HTML: PHP provides the
htmlspecialchars
function for this. PrestoPHP provides a shortcutescape
method:use Symfony\Component\HttpFoundation\Request; $app->get('/name', function (Request $request, PrestoPHP\Application $app) { $name = $request->get('name'); return "You provided the name {$app->escape($name)}."; });
If you use the Twig template engine, you should use its escaping or even auto-escaping mechanisms. Check out the Providers chapter for details.
Escaping JSON: If you want to provide data in JSON format you should use the PrestoPHP
json
function:use Symfony\Component\HttpFoundation\Request; $app->get('/name.json', function (Request $request, PrestoPHP\Application $app) { $name = $request->get('name'); return $app->json(array('name' => $name)); });