diff options
Diffstat (limited to 'src/Controller')
-rw-r--r-- | src/Controller/CheckRequest.php | 86 | ||||
-rw-r--r-- | src/Controller/CheckResponse.php | 75 | ||||
-rw-r--r-- | src/Controller/Respond.php | 307 | ||||
-rw-r--r-- | src/Controller/RouteAction.php | 87 | ||||
-rw-r--r-- | src/Controller/Session.php | 65 | ||||
-rw-r--r-- | src/Controller/Session/Flash.php | 129 | ||||
-rw-r--r-- | src/Controller/View/Twig.php | 178 |
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); + } +} |