summaryrefslogtreecommitdiffstats
path: root/src/Controller
diff options
context:
space:
mode:
authorArnold Daniels <arnold@jasny.net>2016-11-17 21:40:25 +0100
committerArnold Daniels <arnold@jasny.net>2016-11-17 21:40:25 +0100
commit1d84d01f35547bb53a78f33d41a8002f12cbf18c (patch)
treefd9892a7998db822b05229abb7ad06ead8dd58ed /src/Controller
parentc04808754395a7c0454921dcd2e14596bea20745 (diff)
downloadcontroller-1d84d01f35547bb53a78f33d41a8002f12cbf18c.zip
controller-1d84d01f35547bb53a78f33d41a8002f12cbf18c.tar.gz
controller-1d84d01f35547bb53a78f33d41a8002f12cbf18c.tar.bz2
Refactor controller. Move stuff to traits.
Diffstat (limited to 'src/Controller')
-rw-r--r--src/Controller/CheckRequest.php86
-rw-r--r--src/Controller/CheckResponse.php75
-rw-r--r--src/Controller/Respond.php307
-rw-r--r--src/Controller/RouteAction.php87
-rw-r--r--src/Controller/Session.php65
-rw-r--r--src/Controller/Session/Flash.php129
-rw-r--r--src/Controller/View/Twig.php178
7 files changed, 892 insertions, 35 deletions
diff --git a/src/Controller/CheckRequest.php b/src/Controller/CheckRequest.php
new file mode 100644
index 0000000..a63a2b7
--- /dev/null
+++ b/src/Controller/CheckRequest.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Jasny\Controller;
+
+use Psr\Http\Message\ServerRequestInterface;
+
+/**
+ * Controller methods to check the request
+ */
+trait CheckRequest
+{
+ /**
+ * Get request, set for controller
+ *
+ * @return ServerRequestInterface
+ */
+ abstract protected function getRequest();
+
+
+ /**
+ * Check if request is GET request
+ *
+ * @return boolean
+ */
+ public function isGetRequest()
+ {
+ $method = $this->getRequest()->getMethod();
+
+ return !$method || $method === 'GET';
+ }
+
+ /**
+ * Check if request is POST request
+ *
+ * @return boolean
+ */
+ public function isPostRequest()
+ {
+ return $this->getRequest()->getMethod() === 'POST';
+ }
+
+ /**
+ * Check if request is PUT request
+ *
+ * @return boolean
+ */
+ public function isPutRequest()
+ {
+ return $this->getRequest()->getMethod() === 'PUT';
+ }
+
+ /**
+ * Check if request is DELETE request
+ *
+ * @return boolean
+ */
+ public function isDeleteRequest()
+ {
+ return $this->getRequest()->getMethod() === 'DELETE';
+ }
+
+ /**
+ * Check if request is HEAD request
+ *
+ * @return boolean
+ */
+ public function isHeadRequest()
+ {
+ return $this->getRequest()->getMethod() === 'HEAD';
+ }
+
+
+ /**
+ * Returns the HTTP referer if it is on the current host
+ *
+ * @return string
+ */
+ public function getLocalReferer()
+ {
+ $request = $this->getRequest();
+ $referer = $request->getHeaderLine('HTTP_REFERER');
+ $host = $request->getHeaderLine('HTTP_HOST');
+
+ return $referer && parse_url($referer, PHP_URL_HOST) === $host ? $referer : '';
+ }
+} \ No newline at end of file
diff --git a/src/Controller/CheckResponse.php b/src/Controller/CheckResponse.php
new file mode 100644
index 0000000..7c02d46
--- /dev/null
+++ b/src/Controller/CheckResponse.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Jasny\Controller;
+
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Methods to check the response
+ */
+trait CheckResponse
+{
+ /**
+ * Get response. set for controller
+ *
+ * @return ResponseInterface
+ */
+ abstract protected function getResponse();
+
+
+ /**
+ * Check if response is 2xx succesful, or empty
+ *
+ * @return boolean
+ */
+ public function isSuccessful()
+ {
+ $code = $this->getResponse()->getStatusCode() ?: 200;
+
+ return !$code || ($code >= 200 && $code < 300);
+ }
+
+ /**
+ * Check if response is a 3xx redirect
+ *
+ * @return boolean
+ */
+ public function isRedirection()
+ {
+ $code = $this->getResponse()->getStatusCode() ?: 200;
+
+ return $code >= 300 && $code < 400;
+ }
+
+ /**
+ * Check if response is a 4xx client error
+ *
+ * @return boolean
+ */
+ public function isClientError()
+ {
+ $code = $this->getResponse()->getStatusCode() ?: 200;
+
+ return $code >= 400 && $code < 500;
+ }
+
+ /**
+ * Check if response is a 5xx redirect
+ *
+ * @return boolean
+ */
+ public function isServerError()
+ {
+ return $this->getResponse()->getStatusCode() ?: 200 >= 500;
+ }
+
+ /**
+ * Check if response is 4xx or 5xx error
+ *
+ * @return boolean
+ */
+ public function isError()
+ {
+ return $this->isClientError() || $this->isServerError();
+ }
+}
diff --git a/src/Controller/Respond.php b/src/Controller/Respond.php
new file mode 100644
index 0000000..02b2cfb
--- /dev/null
+++ b/src/Controller/Respond.php
@@ -0,0 +1,307 @@
+<?php
+
+namespace Jasny\Controller;
+
+use Psr\Http\Message\ResponseInterface;
+use Dflydev\ApacheMimeTypes\PhpRepository as ApacheMimeTypes;
+
+/**
+ * Methods for a controller to send a response
+ */
+trait Respond
+{
+ /**
+ * Get response. set for controller
+ *
+ * @return ResponseInterface
+ */
+ abstract protected function getResponse();
+
+ /**
+ * Get response. set for controller
+ *
+ * @return ResponseInterface
+ */
+ abstract protected function setResponse(ResponseInterface $response);
+
+ /**
+ * Returns the HTTP referer if it is on the current host
+ *
+ * @return string
+ */
+ abstract public function getLocalReferer();
+
+
+ /**
+ * Set a response header
+ *
+ * @param string $header
+ * @param string $value
+ * @param boolean $overwrite
+ */
+ public function setResponseHeader($header, $value, $overwrite = true)
+ {
+ $fn = $overwrite ? 'withHeader' : 'withAddedHeader';
+ $response = $this->getResponse()->$fn($value, $header);
+
+ $this->setResponse($response);
+ }
+
+ /**
+ * Set the headers with HTTP status code and content type.
+ * @link http://en.wikipedia.org/wiki/List_of_HTTP_status_codes
+ *
+ * Examples:
+ * <code>
+ * $this->respondWith(200, 'json');
+ * $this->respondWith(200, 'application/json');
+ * $this->respondWith(204);
+ * $this->respondWith("204 Created");
+ * $this->respondWith('json');
+ * </code>
+ *
+ * @param int $code HTTP status code (may be omitted)
+ * @param string|array $format Mime or content format
+ */
+ public function respondWith($code, $format = null)
+ {
+ $response = $this->getResponse();
+
+ // Shift arguments if $code is omitted
+ if (!is_int($code) && !preg_match('/^\d{3}\b/', $code)) {
+ list($code, $format) = array_merge([null], func_get_args());
+ }
+
+ if ($code) {
+ $response = $response->withStatus((int)$code);
+ }
+
+ if ($format) {
+ $contentType = $this->getContentType($format);
+ $response = $response->withHeader('Content-Type', $contentType);
+ }
+
+ $this->setResponse($response);
+ }
+
+
+ /**
+ * Response with 200 OK
+ *
+ * @return ResponseInterface $response
+ */
+ public function ok()
+ {
+ $this->respondWith(200);
+ }
+
+ /**
+ * Response with created 201 code, and optionaly redirect to created location
+ *
+ * @param string $location Url of created resource
+ */
+ public function created($location = '')
+ {
+ $this->respondWith(201);
+
+ if ($location) {
+ $this->setResponseHeader('Location', $location);
+ }
+ }
+
+ /**
+ * Response with 204 No Content
+ */
+ public function noContent()
+ {
+ $this->respondWith(204);
+ }
+
+ /**
+ * Redirect to url
+ *
+ * @param string $url
+ * @param int $code 301 (Moved Permanently), 303 (See Other) or 307 (Temporary Redirect)
+ */
+ public function redirect($url, $code = 303)
+ {
+ $this->respondWith($code, 'text/html');
+ $this->setResponseHeader('Location', $url);
+
+ $urlHtml = htmlentities($url);
+ $this->output('You are being redirected to <a href="' . $urlHtml . '">' . $urlHtml . '</a>');
+ }
+
+ /**
+ * Redirect to previous page, or to home page
+ *
+ * @return ResponseInterface $response
+ */
+ public function back()
+ {
+ $this->redirect($this->getLocalReferer() ?: '/');
+ }
+
+
+ /**
+ * Respond with 400 Bad Request
+ *
+ * @param string $message
+ * @param int $code HTTP status code
+ */
+ public function badRequest($message, $code = 400)
+ {
+ $this->respondWith($code);
+ $this->output($message);
+ }
+
+ /**
+ * Respond with a 401 Unauthorized
+ */
+ public function requireAuth()
+ {
+ $this->respondWith(401);
+ }
+
+ /**
+ * Alias of requireAuth
+ * @deprecated
+ */
+ final public function requireLogin()
+ {
+ $this->requireAuth();
+ }
+
+ /**
+ * Respond with 402 Payment Required
+ *
+ * @param string $message
+ */
+ public function paymentRequired($message = "Payment required")
+ {
+ $this->respondWith(402);
+ $this->output($message);
+ }
+
+ /**
+ * Respond with 403 Forbidden
+ *
+ * @param string $message
+ */
+ public function forbidden($message = "Forbidden")
+ {
+ $this->respondWith(403);
+ $this->output($message);
+ }
+
+ /**
+ * Respond with 404 Not Found
+ *
+ * @param string $message
+ * @param int $code HTTP status code (404 or 405)
+ */
+ public function notFound($message = "Not found", $code = 404)
+ {
+ $this->respondWith($code);
+ $this->output($message);
+ }
+
+ /**
+ * Respond with 409 Conflict
+ *
+ * @param string $message
+ */
+ public function conflict($message)
+ {
+ $this->respondWith(409);
+ $this->output($message);
+ }
+
+ /**
+ * Respond with 429 Too Many Requests
+ *
+ * @param string $message
+ */
+ public function tooManyRequests($message = "Too many requests")
+ {
+ $this->respondWith(429);
+ $this->output($message);
+ }
+
+
+ /**
+ * Respond with a server error
+ *
+ * @param string $message
+ * @param int $code HTTP status code
+ */
+ public function error($message = "An unexpected error occured", $code = 500)
+ {
+ $this->respondWith($code);
+ $this->output($message);
+ }
+
+
+ /**
+ * Get MIME type for extension
+ *
+ * @param string $format
+ * @return string
+ */
+ protected function getContentType($format)
+ {
+ if (\Jasny\str_contains($format, '/')) { // Already MIME
+ return $format;
+ }
+
+ $repository = new ApacheMimeTypes();
+ $mime = $repository->findType('html');
+
+ if (!isset($mime)) {
+ throw new \UnexpectedValueException("Format $format doesn't correspond with a MIME type");
+ }
+
+ return $mime;
+ }
+
+ /**
+ * Serialize data
+ *
+ * @param mixed $data
+ * @param string $contentType
+ * @return string
+ */
+ protected function serializeData($data, $contentType)
+ {
+ if ($contentType == 'json') {
+ return (is_string($data) && (json_decode($data) !== null || !json_last_error()))
+ ? $data : json_encode($data);
+ }
+
+ if (!is_scalar($data)) {
+ throw new \Exception("Unable to serialize data to {$contentType}");
+ }
+
+ return $data;
+ }
+
+ /**
+ * Output result
+ *
+ * @param mixed $data
+ * @param string $format Output format as MIME or extension
+ */
+ public function output($data, $format = null)
+ {
+ if (!isset($format)) {
+ $contentType = $this->getResponse()->getHeaderLine('Content-Type') ?: 'text/html';
+ } else {
+ $contentType = $this->getContentType($format);
+ $this->setResponseHeader('Content-Type', $contentType);
+ }
+
+ $content = $this->serializeData($data, $contentType);
+
+ $this->getResponse()->getBody()->write($content);
+ }
+}
diff --git a/src/Controller/RouteAction.php b/src/Controller/RouteAction.php
index dabd501..5303f8a 100644
--- a/src/Controller/RouteAction.php
+++ b/src/Controller/RouteAction.php
@@ -15,40 +15,75 @@ trait RouteAction
*
* @return ServerRequestInterface
*/
- abstract public function getRequest();
+ abstract protected function getRequest();
- /**
+ /**
* Get response. set for controller
*
* @return ResponseInterface
*/
- abstract public function getResponse();
+ abstract protected function getResponse();
+
+ /**
+ * Respond with a server error
+ *
+ * @param string $message
+ * @param int $code HTTP status code
+ */
+ abstract public function notFound($message = '', $code = 404);
/**
+ * Check if response is 2xx succesful, or empty
+ *
+ * @return boolean
+ */
+ abstract public function isSuccessful();
+
+
+ /**
+ * Called before executing the action.
+ * If the response is no longer a success statuc (>= 300), the action will not be executed.
+ *
+ * <code>
+ * protected function beforeAction()
+ * {
+ * $this->respondWith('json'); // Respond with JSON by default
+ *
+ * if ($this->auth->getUser()->getCredits() <= 0) {
+ * $this->paymentRequired();
+ * }
+ * }
+ * </code>
+ */
+ protected function beforeAction()
+ {
+ }
+
+ /**
* Run the controller
*
* @return ResponseInterface
*/
- public function run() {
- $request = $this->getRequest();
- $route = $request->getAttribute('route');
+ public function run()
+ {
+ $route = $this->getRequest()->getAttribute('route');
$method = $this->getActionMethod(isset($route->action) ? $route->action : 'default');
-
+
if (!method_exists($this, $method)) {
- return $this->setResponseError(404, 'Not Found');
+ return $this->notFound();
}
- try {
- $args = isset($route->args) ?
- $route->args :
- $this->getFunctionArgs($route, new \ReflectionMethod($this, $method));
- } catch (\RuntimeException $e) {
- return $this->setResponseError(400, 'Bad Request');
- }
+ $args = isset($route->args)
+ ? $route->args
+ : $this->getFunctionArgs($route, new \ReflectionMethod($this, $method));
- $response = call_user_func_array([$this, $method], $args);
+ $this->beforeAction();
+
+ if ($this->isSuccessful()) {
+ call_user_func_array([$this, $method], $args);
+ }
- return $response ?: $this->getResponse();
+ return $this->getResponse();
}
/**
@@ -96,22 +131,4 @@ trait RouteAction
return $args;
}
-
- /**
- * Set response to error state
- *
- * @param int $code
- * @param string $message
- * @return ResponseInterface
- */
- protected function setResponseError($code, $message)
- {
- $response = $this->getResponse();
-
- $errorResponse = $response->withStatus($code);
- $errorResponse->getBody()->write($message);
-
- return $errorResponse;
- }
}
-
diff --git a/src/Controller/Session.php b/src/Controller/Session.php
new file mode 100644
index 0000000..8dc292f
--- /dev/null
+++ b/src/Controller/Session.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Jasny\Controller;
+
+use Jasny\Controller\Session\Flash;
+
+/**
+ * Use session in the controller
+ */
+trait Session
+{
+ /**
+ * Session
+ * @var array|\ArrayObject
+ */
+ protected $session;
+
+ /**
+ * Flash message
+ * @var Flash
+ */
+ protected $flash;
+
+
+ /**
+ * Get request, set for controller
+ *
+ * @return ServerRequestInterface
+ */
+ abstract protected function getRequest();
+
+
+ /**
+ * Link the session to the session property in the controller
+ */
+ protected function useSession()
+ {
+ $this->session = $this->getRequest()->getAttribute('session');
+
+ if (!isset($this->session)) {
+ $this->session =& $_SESSION;
+ }
+ }
+
+
+ /**
+ * Get an/or set the flash message.
+ *
+ * @param mixed $type flash type, eg. 'error', 'notice' or 'success'
+ * @param mixed $message flash message
+ * @return Flash
+ */
+ public function flash($type = null, $message = null)
+ {
+ if (!isset($this->flash)) {
+ $this->flash = new Flash($this->session);
+ }
+
+ if ($type) {
+ $this->flash->set($type, $message);
+ }
+
+ return $this->flash;
+ }
+}
diff --git a/src/Controller/Session/Flash.php b/src/Controller/Session/Flash.php
new file mode 100644
index 0000000..be72e7c
--- /dev/null
+++ b/src/Controller/Session/Flash.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace Jasny\Controller\Session;
+
+/**
+ * Class for the flash message
+ */
+class Flash
+{
+ /**
+ * @var array
+ */
+ protected $data;
+
+ /**
+ * @var array|\ArrayObject
+ */
+ protected $session;
+
+ /**
+ * Session key for flash
+ * @var string
+ */
+ protected $key = 'flash';
+
+
+ /**
+ * Class constructor
+ *
+ * @param array|\ArrayObject $session
+ */
+ public function __construct(&$session)
+ {
+ $this->session =& $session;
+ }
+
+ /**
+ * Check if the flash is set.
+ *
+ * @return boolean
+ */
+ public function isIssued()
+ {
+ return isset($this->session[$this->key]);
+ }
+
+ /**
+ * Set the flash.
+ *
+ * @param string $type flash type, eg. 'error', 'notice' or 'success'
+ * @param mixed $message flash message
+ */
+ public function set($type, $message)
+ {
+ if (!$type) {
+ throw new \InvalidArgumentException("Type should not be empty");
+ }
+
+ $this->session[$this->key] = compact('type', 'message');
+ }
+
+ /**
+ * Get the flash.
+ *
+ * @return object
+ */
+ public function get()
+ {
+ if (!isset($this->data) && isset($this->session[$this->key])) {
+ $this->data = $this->session[$this->key];
+ unset($this->session[$this->key]);
+ }
+
+ return (object)$this->data;
+ }
+
+ /**
+ * Reissue the flash.
+ */
+ public function reissue()
+ {
+ if (!isset($this->data) && isset($this->session[$this->key])) {
+ $this->data = $this->session[$this->key];
+ } else {
+ $this->session[$this->key] = $this->data;
+ }
+ }
+
+ /**
+ * Clear the flash.
+ */
+ public function clear()
+ {
+ $this->data = null;
+ unset($this->session[$this->key]);
+ }
+
+ /**
+ * Get the flash type
+ *
+ * @return string
+ */
+ public function getType()
+ {
+ $data = $this->get();
+ return isset($data) ? $data->type : null;
+ }
+
+ /**
+ * Get the flash message
+ *
+ * @return string
+ */
+ public function getMessage()
+ {
+ $data = $this->get();
+ return isset($data) ? $data->message : null;
+ }
+
+ /**
+ * Cast object to string
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return (string)$this->getMessage();
+ }
+}
diff --git a/src/Controller/View/Twig.php b/src/Controller/View/Twig.php
new file mode 100644
index 0000000..ee5d4ce
--- /dev/null
+++ b/src/Controller/View/Twig.php
@@ -0,0 +1,178 @@
+<?php
+
+namespace Jasny\Controller\View;
+
+use Jasny\Flash;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * View using Twig
+ */
+trait Twig
+{
+ /**
+ * Twig environment
+ * @var \Twig_Environment
+ */
+ protected $twig = null;
+
+ /**
+ * Get server request
+ * @return ServerRequestInterface
+ */
+ abstract public function getRequest();
+
+ /**
+ * Get server response
+ * @return ResponseInterface
+ */
+ abstract public function getResponse();
+
+ /**
+ * Add a global variable to the view.
+ *
+ * @param string $name Variable name
+ * @param mixed $value
+ * @return $this
+ */
+ public function setViewVariable($name, $value)
+ {
+ if (!$name) throw new \InvalidArgumentException("Name should not be empty");
+
+ $this->getTwig()->addGlobal($name, $value);
+
+ return $this;
+ }
+
+ /**
+ * Expose a function to the view.
+ *
+ * @param string $name Variable name
+ * @param mixed $function
+ * @param string $as 'function' or 'filter'
+ * @return $this
+ */
+ public function setViewFunction($name, $function = null, $as = 'function')
+ {
+ if ($as === 'function') {
+ $this->getTwig()->addFunction($this->createTwigFunction($name, $function));
+ } elseif ($as === 'filter') {
+ $this->getTwig()->addFilter($this->createTwigFilter($name, $function));
+ } else {
+ throw new \InvalidArgumentException("You should create either function or filter, not '$as'");
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add extension to view
+ *
+ * @param object $extension
+ * @return $this
+ */
+ public function setViewExtension($extension)
+ {
+ $this->getTwig()->addExtension($extension);
+
+ return $this;
+ }
+
+ /**
+ * View rendered template
+ *
+ * @param string $name Template name
+ * @param array $context Template context
+ * @return ResponseInterface
+ */
+ public function view($name, array $context = [])
+ {
+ if (!pathinfo($name, PATHINFO_EXTENSION)) $name .= '.html.twig';
+
+ $twig = $this->getTwig();
+ $tmpl = $twig->loadTemplate($name);
+
+ $response = $this->getResponse();
+ $response = $response->withHeader('Content-Type', 'text/html; charset=' . $twig->getCharset());
+ $response->getBody()->write($tmpl->render($context));
+
+ return $response;
+ }
+
+ /**
+ * Get twig environment
+ *
+ * @return \Twig_Environment
+ */
+ public function getTwig()
+ {
+ if ($this->twig) return $this->twig;
+
+ $loader = $this->getTwigLoader();
+ $this->twig = $this->getTwigEnvironment($loader);
+
+ $extensions = ['DateExtension', 'PcreExtension', 'TextExtension', 'ArrayExtension'];
+ foreach ($extensions as $name) {
+ $class = "Jasny\Twig\\$name";
+
+ if (class_exists($class)) $this->setViewExtension(new $class());
+ }
+
+ $uri = $this->getRequest()->getUri()->getPath();
+
+ $this->setViewVariable('current_url', $uri);
+ $this->setViewVariable('flash', new Flash());
+
+ return $this->twig;
+ }
+
+ /**
+ * Get twig loasder for current working directory
+ *
+ * @return \Twig_Loader_Filesystem
+ */
+ public function getTwigLoader()
+ {
+ return new \Twig_Loader_Filesystem(getcwd());
+ }
+
+ /**
+ * Get twig environment instance
+ *
+ * @param \Twig_Loader_Filesystem $loader
+ * @return \Twig_Environment
+ */
+ public function getTwigEnvironment(\Twig_Loader_Filesystem $loader)
+ {
+ return new \Twig_Environment($loader);
+ }
+
+ /**
+ * Create twig function
+ *
+ * @param string $name Name of function in view
+ * @param callable|null $function
+ * @return \Twig_SimpleFunction
+ */
+ public function createTwigFunction($name, $function)
+ {
+ if (!$name) throw new \InvalidArgumentException("Function name should not be empty");
+
+ return new \Twig_SimpleFunction($name, $function ?: $name);
+ }
+
+ /**
+ * Create twig filter
+ *
+ * @param string $name Name of filter in view
+ * @param callable|null $function
+ * @return \Twig_SimpleFilter
+ */
+ public function createTwigFilter($name, $function)
+ {
+ if (!$name) throw new \InvalidArgumentException("Filter name should not be empty");
+
+ return new \Twig_SimpleFilter($name, $function ?: $name);
+ }
+}