Things go wrong. You can’t predict errors, but you can anticipate them. Each Slim Framework application has an error handler that receives all uncaught PHP exceptions. This error handler also receives the current HTTP request and response objects, too. The error handler must prepare and return an appropriate Response object to be returned to the HTTP client.
Usage
<?php
use SlimFactoryAppFactory;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
/**
* The routing middleware should be added earlier than the ErrorMiddleware
* Otherwise exceptions thrown from it will not be handled by the middleware
*/
$app->addRoutingMiddleware();
/**
* Add Error Middleware
*
* @param bool $displayErrorDetails -> Should be set to false in production
* @param bool $logErrors -> Parameter is passed to the default ErrorHandler
* @param bool $logErrorDetails -> Display error details in error log
* @param LoggerInterface|null $logger -> Optional PSR-3 Logger
*
* Note: This middleware should be added last. It will not handle any exceptions/errors
* for middleware added after it.
*/
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
// ...
$app->run();
Adding Custom Error Handlers
You can now map custom handlers for any type of Exception or Throwable.
<?php
use PsrHttpMessageServerRequestInterface;
use PsrLogLoggerInterface;
use SlimFactoryAppFactory;
use SlimPsr7Response;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
// Add Routing Middleware
$app->addRoutingMiddleware();
// Define Custom Error Handler
$customErrorHandler = function (
ServerRequestInterface $request,
Throwable $exception,
bool $displayErrorDetails,
bool $logErrors,
bool $logErrorDetails,
?LoggerInterface $logger = null
) use ($app) {
$logger->error($exception->getMessage());
$payload = ['error' => $exception->getMessage()];
$response = $app->getResponseFactory()->createResponse();
$response->getBody()->write(
json_encode($payload, JSON_UNESCAPED_UNICODE)
);
return $response;
};
// Add Error Middleware
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
$errorMiddleware->setDefaultErrorHandler($customErrorHandler);
// ...
$app->run();
Error Logging
If you would like to pipe in custom error logging to the default ErrorHandler
that ships with Slim, there are two ways to do it.
With the first method, you can simply extend ErrorHandler
and stub the logError()
method.
<?php
namespace MyAppHandlers;
use SlimHandlersErrorHandler;
class MyErrorHandler extends ErrorHandler
{
protected function logError(string $error): void
{
// Insert custom error logging function.
}
}
<?php
use MyAppHandlersMyErrorHandler;
use SlimFactoryAppFactory;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
// Add Routing Middleware
$app->addRoutingMiddleware();
// Instantiate Your Custom Error Handler
$myErrorHandler = new MyErrorHandler($app->getCallableResolver(), $app->getResponseFactory());
// Add Error Middleware
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
$errorMiddleware->setDefaultErrorHandler($myErrorHandler);
// ...
$app->run();
With the second method, you can supply a logger that conforms to the
PSR-3 standard, such as one from the popular
Monolog library.
<?php
use MonologHandlerStreamHandler;
use MonologLogger;
use MyAppHandlersMyErrorHandler;
use SlimFactoryAppFactory;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
// Add Routing Middleware
$app->addRoutingMiddleware();
// Monolog Example
$logger = new Logger('app');
$streamHandler = new StreamHandler(__DIR__ . '/var/log', 100);
$logger->pushHandler($streamHandler);
// Add Error Middleware with Logger
$errorMiddleware = $app->addErrorMiddleware(true, true, true, $logger);
// ...
$app->run();
Error Handling/Rendering
The rendering is finally decoupled from the handling.
It will still detect the content-type and render things appropriately with the help of ErrorRenderers
.
The core ErrorHandler
extends the AbstractErrorHandler
class which has been completely refactored.
By default it will call the appropriate ErrorRenderer
for the supported content types. The core
ErrorHandler
defines renderers for the following content types:
application/json
application/xml
andtext/xml
text/html
text/plain
For any content type you can register your own error renderer. So first define a new error renderer
that implements SlimInterfacesErrorRendererInterface
.
<?php
use SlimInterfacesErrorRendererInterface;
use Throwable;
class MyCustomErrorRenderer implements ErrorRendererInterface
{
public function __invoke(Throwable $exception, bool $displayErrorDetails): string
{
return 'My awesome format';
}
}
And then register that error renderer in the core error handler. In the example below we
will register the renderer to be used for text/html
content types.
<?php
use MyAppHandlersMyErrorHandler;
use SlimFactoryAppFactory;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
// Add Routing Middleware
$app->addRoutingMiddleware();
// Add Error Middleware
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
// Get the default error handler and register my custom error renderer.
$errorHandler = $errorMiddleware->getDefaultErrorHandler();
$errorHandler->registerErrorRenderer('text/html', MyCustomErrorRenderer::class);
// ...
$app->run();
Force a specific content type for error rendering
By default, the error handler tries to detect the error renderer using the Accept
header of the
request. If you need to force the error handler to use a specific error renderer you can
write the following.
$errorHandler->forceContentType('application/json');
New HTTP Exceptions
We have added named HTTP exceptions within the application. These exceptions work nicely with the native renderers. They can each have a description
and title
attribute as well to provide a bit more insight when the native HTML renderer is invoked.
The base class HttpSpecializedException
extends Exception
and comes with the following sub classes:
- HttpBadRequestException
- HttpForbiddenException
- HttpInternalServerErrorException
- HttpMethodNotAllowedException
- HttpNotFoundException
- HttpNotImplementedException
- HttpUnauthorizedException
You can extend the HttpSpecializedException
class if they need any other response codes that we decide not to provide with the base repository. Example if you wanted a 504 gateway timeout exception that behaves like the native ones you would do the following:
class HttpForbiddenException extends HttpSpecializedException
{
protected $code = 504;
protected $message = 'Gateway Timeout.';
protected $title = '504 Gateway Timeout';
protected $description = 'Timed out before receiving response from the upstream server.';
}
One of the nice things about Slim 4 is that it’s easier to customise the HTML generated on error without having to worry about the rest of the error handling mechanism. This is because we have separated error rendering from error handling.
Slim’s default ErrorHandler maintains a list of renderers, one for each content-type and will delegate the creation of the error payload (HTML, JSON, XML, etc) to the renderer. Out of the Box, the error hander registers an HtmlErrorRenderer which provides a very basic error display:
We can also enable the displayErrorDetails setting to view the information about the error:
Separately, the error handler can log these details to the error log.
Writing a custom HTML renderer
To write a new error renderer, we need to create a classs that implements SLimInterfacesErrorRendererInterface. This interface requires a single method:
public function __invoke(Throwable $exception, bool $displayErrorDetails): string;
We are given the exception that caused this error and have to return the HTML we wish to be displayed.
Let’s create our own HTML error renderer. If you want to play along, my Slim4-Starter project is a good place to start from.
We start with a basic HtmlErrorRenderer, which we’ll store in the src/Error/Renderer directory:
<?php declare(strict_types=1); namespace AppErrorRenderer; use SlimExceptionHttpNotFoundException; use SlimInterfacesErrorRendererInterface; use Throwable; final class HtmlErrorRenderer extends ErrorRendererInterface { public function __invoke(Throwable $exception, bool $displayErrorDetails): string { $title = 'Error'; $message = 'An error has occurred.'; if ($exception instanceof HttpNotFoundException) { $title = 'Page not found'; $message = 'This page could not be found.'; } return $this->renderHtmlPage($title, $message); } public function renderHtmlPage(string $title = '', string $message = ''): string { $title = htmlentities($title, ENT_COMPAT|ENT_HTML5, 'utf-8'); $message = htmlentities($message, ENT_COMPAT|ENT_HTML5, 'utf-8'); return <<<EOT <!DOCTYPE html> <html> <head> <title>$title - My website</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mini.css/3.0.1/mini-default.css"> </head> <body> <h1>$title</h1> <p>$message</p> </body> </html> EOT; } }
We can do whatever we like in __invoke(). In this case, I have chosen to change the title and message based on the class of the $exception. Slim will show a HttpNotFoundException when the router cannot find a registered route for the request URL, so we can use this to show different text explaining the problem.
We also need to render the HTML itself. This is likely to match the rest of your website’s design, so the implementation of renderHtmlPage() is entirely up to you. I’m sure you can do better than I can though!
Register our renderer
We register our renderer, at the point we add the ErrorMiddleare, which in my case is in config/middleware.php:
$displayErrorDetails = (bool)($_ENV['DEBUG'] ?? false); $errorMiddleware = $app->addErrorMiddleware($displayErrorDetails, true, true); $errorHandler = $errorMiddleware->getDefaultErrorHandler(); $errorHandler->registerErrorRenderer('text/html', HtmlErrorRenderer::class);
Don’t forget to add use AppErrorRendererHtmlErrorRenderer; at the top of the file and now the new error renderer will now be used.
This is what our new error page looks like:
Using Whoops for development
When we’re developing, it would be useful to have more information about what’s gone wrong. The whoops error handler can provide this, so let’s add it. Fortunately for us, the hard work has been done already by Zeusis in their zeuxisoo/slim-whoops package, so just install it:
$ composer require "zeuxisoo/slim-whoops"
We can now add the WhoopsMiddleare if we are in debug mode:
if ((bool)($ENV_['DEBUG'] ?? false)) { $app->add(new WhoopsMiddleware(['enable' => true])); } else { $errorMiddleware = $app->addErrorMiddleware(false, true, true); $errorHandler = $errorMiddleware->getDefaultErrorHandler(); $errorHandler->registerErrorRenderer('text/html', HtmlErrorRenderer::class); }
I’ve triggered it on the DEBUG environment variable, bu too course, you should use whatever method works for you in order to determine when the app is in development mode.
To sum up
That’s it. Changing the error rendering ensure that your error pages look part of your website and it’s easy to do so. In addition, the technique can also be used to render custom error payloads for APIs by registering error renderers for the error handler’s application/json and application/xml content types.
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and
privacy statement. We’ll occasionally send you account related emails.
Already on GitHub?
Sign in
to your account
Closed
lautiamkok opened this issue
Jul 17, 2019
· 9 comments
Comments
How can we handle errors by ourselves completely?
I get this error below:
Uncaught SlimExceptionHttpNotFoundException: Not found.
If I remove these lines:
// $callableResolver = $app->getCallableResolver();
// $responseFactory = $app->getResponseFactory();
// $errorMiddleware = new ErrorMiddleware($callableResolver, $responseFactory, true, true, true);
// $app->add($errorMiddleware);
Then, when we try to catch to the error through a middleware:
$app->add(function (Request $request, RequestHandler $handler) {
try {
$response = $handler->handle($request);
$existingContent = (string) $response->getBody();
...
} catch (Exception $error) {
$response = $handler->handle($error->getRequest());
$data = [
"status" => $error->getCode(),
"messsage" => $error->getMessage()
];
$payload = json_encode($data);
$response->getBody()->write($payload);
return $response
->withHeader('Content-Type', 'application/json')
->withStatus($status);
};
});
We get the same error back:
Uncaught SlimExceptionHttpNotFoundException: Not found.
We are trying to make our custom error in the JSON format, for example:
{"status":404,"message":"page not found"}
Any ideas?
By calling the handle()
method of the handler in the catch-block again, the handler would throw the exception again. But this time, no one is there to catch this second exception.
In Slim 4 you could either define your custom error handler (and pass it to the ErrorMiddleware
) or simply define custom error renderers (based on accepted content-types by your request or on a forced content-type).
More information can be found in the docs, currently in development: https://github.com/slimphp/Slim-Website/blob/gh-pages-4.x/docs/v4/middleware/error-handling.md
@adriansuter none of that actually works. giving up…
In my view, this should be made simpler. But it is way too complicated now.
@lautiamkok it does work, I think you’re doing something wrong on your end. Where are you adding this middleware? It should be the very last middleware you add, right before that middleware should be the routing middleware, otherwise Slim automatically appends the RoutingMiddleware
if it hasn’t been appended manually, which means it would go in front of your error handling middleware, which means you wouldn’t catch anything.
@l0gicgate let me check later again and get back to you later. there are some errors in the code in the doc by the way
slim should have left all the error handling alone from the year dot. how it handles errors just does not make sense. for example in slim 3.12, if i want to handle 404 and other errors myself, I have to write two blocks of code:
$container = new SlimContainer();
$container['errorHandler'] = function ($container) {
return function ($request, $response, $exception) use ($container) {
// print_r(get_class_methods($exception));
$message = $exception->getMessage();
$code = $exception->getCode();
$data = [
"status" => $code,
"message" => $message
];
$payload = json_encode($data);
$response->getBody()->write($payload);
return $response
->withHeader('Content-Type', 'application/json')
->withStatus($code);
};
};
$container['notFoundHandler'] = function ($container) {
return function ($request, $response) use ($container) {
return $response->withStatus(404)
->withHeader('Content-Type', 'text/html')
->write('Page not found');
};
};
It should just be one block of code. also, errorHandler
and notFoundHandler
work differently. they should work the same way.
My personal view on these lines below for handling error in slim 4:
$callableResolver = $app->getCallableResolver();
$responseFactory = $app->getResponseFactory();
$errorMiddleware = new ErrorMiddleware($callableResolver, $responseFactory, true, true, true);
$app->add($errorMiddleware);
I am not sure if this is a PSR standard or not but it is too ugly to read and to understand what is going on.
I don’t know what these two $callableResolver
and $responseFactory
are doing there. if they are not helping us to understand by reading them, they shouldn’t be there.
this is just my opinion.
That is dependency injection. As the middleware needs to have the callable resolver and the response factory, they would be given to the middleware. That way, the middleware is as independent as possible from the rest.
I’m closing this as resolved. The docs are clear. If you dislike the architecture you can always use a different framework. You’re also more than welcome to contribute when those architecture decisions are being made, the PRs and discussions are public.
gmariani, HarryPi, stutommi, jboilesen, Rotzbua, cameronsteele, Franweb79, idle-user, ajpgtech, Thifany-Nicastro, and 2 more reacted with thumbs down emoji
Another way to handle this is to define a default route after all your other route definitions:
$app->any('{route:.*}', function(Request $request, Response $response) { $response = $response->withStatus(404, 'page not found'); return $response; });
Cvar1984, hadamlenz, sudofox, and KevP reacted with rocket emoji
I think a good answer would be to show exactly how a way to disable ALL error handling of any kind since I’m sure slim is already using specific exception types internally that can easily be caught. Or show how to specify a single error handler for all events so a controller view can be used and just display a 404/500 or whatever else message it needs to show accordingly.
Just saying docs are clear with no reference and if you don’t like it move along isn’t a great response.
If there is no way to do this I’d agree it is just too complicated. Error handling is so simple.
The Slim Framework is good — seriously good. One of the issues I have run into is with the way it handles errors. In vanilla PHP code I occasionally use trigger_error statements as a debug aid. In vanilla PHP this has no untoward consequences since by default trigger_errors are E_USER_NOTICE type errors that do not stop the script dead in its tracks. However, in Slim things appear to work differently. A benign trigger_error causes it to throw a wobbly and an HTTP 500 is returned.
I thought this could be corrected by
- Changing the mode to development or something but the docs state that this makes no difference whatsoever to the way Slim works internally.
-
Next port of call — changing the slim error logging level
$app = new SlimSlim(array(‘log.level’ => SlimLog::ERROR);
does not quite have the same effect as PHP’s error_reporting. Setting it stops the error from floating up to the error.log file (the default error logger used by Slim) but crucially it does not stop the HTTP 500.
I have come across forum posts that suggest replacing the default Slim::handleErrors method. That would be easy but I wonder if that is not incorrect. What is the right way to stop Slim coming to a dead halt when it runs into a wholly innocuous trigger_error? I can well avoid this but I may rely on other code that may have such statements. I would much appreciate any help
asked Dec 16, 2014 at 14:37
3
The answer turns out to be quite simple. I figured it out by checking out the handleErrors function in slim.php. Just issue a
error_reporting(E_ALL & ~E_USER_WARNING);
prior to where the trigger_error is called and you are in business. Somewhere down the line Slim is changing the default PHP error_reporting to include E_USER_WARNING.
answered Dec 16, 2014 at 21:33
DroidOSDroidOS
8,31016 gold badges94 silver badges165 bronze badges
Я использую lampp на своей Linux-машине для размещения веб-сайта. БД настроена как виртуальный хост. Кроме того, зависимости php исправлены с помощью composer. Когда я запускаю компоненты lampp и захожу на локахост, я получаю эту ошибку. Я пытался как-то это исправить, но ничего не получалось.
Надеюсь, вы можете мне помочь, спасибо.
Ошибка тонкого приложения
The application could not run because of the following error:
Details
Type: UnexpectedValueException
Message: The stream or file «../logs/app.log» could not be opened: failed to open stream: Permission denied
File: /opt/lampp/htdocs/starlight-app/vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php
Line: 107
Trace0 /opt/lampp/htdocs/starlight-app/vendor/monolog/monolog/src/Monolog/Handler/AbstractProcessingHandler.php(39): MonologHandlerStreamHandler->write(Array)
1 /opt/lampp/htdocs/starlight-app/vendor/monolog/monolog/src/Monolog/Logger.php(344): MonologHandlerAbstractProcessingHandler->handle(Array)
2 /opt/lampp/htdocs/starlight-app/vendor/monolog/monolog/src/Monolog/Logger.php(637): MonologLogger->addRecord(200, ‘Loading route.’, Array)
3 /opt/lampp/htdocs/starlight-app/config/middlewares.php(94): MonologLogger->info(‘Loading route.’, Array)
4 [internal function]: Closure->{closure}(Object(SlimHttpRequest), Object(SlimHttpResponse), Object(Closure))
5 /opt/lampp/htdocs/starlight-app/vendor/slim/slim/Slim/DeferredCallable.php(43): call_user_func_array(Object(Closure), Array)
6 [internal function]: SlimDeferredCallable->__invoke(Object(SlimHttpRequest), Object(SlimHttpResponse), Object(Closure))
7 /opt/lampp/htdocs/starlight-app/vendor/slim/slim/Slim/MiddlewareAwareTrait.php(70): call_user_func(Object(SlimDeferredCallable), Object(SlimHttpRequest), Object(SlimHttpResponse), Object(Closure))
8 /opt/lampp/htdocs/starlight-app/vendor/slim/csrf/src/Guard.php(171): SlimApp->Slim{closure}(Object(SlimHttpRequest), Object(SlimHttpResponse))
9 [internal function]: SlimCsrfGuard->__invoke(Object(SlimHttpRequest), Object(SlimHttpResponse), Object(Closure))
10 /opt/lampp/htdocs/starlight-app/vendor/slim/slim/Slim/DeferredCallable.php(43): call_user_func_array(Object(SlimCsrfGuard), Array)
11 [internal function]: SlimDeferredCallable->__invoke(Object(SlimHttpRequest), Object(SlimHttpResponse), Object(Closure))
12 /opt/lampp/htdocs/starlight-app/vendor/slim/slim/Slim/MiddlewareAwareTrait.php(70): call_user_func(Object(SlimDeferredCallable), Object(SlimHttpRequest), Object(SlimHttpResponse), Object(Closure))
13 /opt/lampp/htdocs/starlight-app/config/middlewares.php(178): SlimApp->Slim{closure}(Object(SlimHttpRequest), Object(SlimHttpResponse))
14 [internal function]: Closure->{closure}(Object(SlimHttpRequest), Object(SlimHttpResponse), Object(Closure))
15 /opt/lampp/htdocs/starlight-app/vendor/slim/slim/Slim/DeferredCallable.php(43): call_user_func_array(Object(Closure), Array)
16 [internal function]: SlimDeferredCallable->__invoke(Object(SlimHttpRequest), Object(SlimHttpResponse), Object(Closure))
17 /opt/lampp/htdocs/starlight-app/vendor/slim/slim/Slim/MiddlewareAwareTrait.php(70): call_user_func(Object(SlimDeferredCallable), Object(SlimHttpRequest), O
Object(SlimHttpResponse), Object(Closure))18 /opt/lampp/htdocs/starlight-app/vendor/slim/slim/Slim/MiddlewareAwareTrait.php(117): SlimApp->Slim{closure}(Object(SlimHttpRequest), Object(SlimHttpResponse))
19 /opt/lampp/htdocs/starlight-app/vendor/slim/slim/Slim/App.php(405): SlimApp->callMiddlewareStack(Object(SlimHttpRequest), Object(SlimHttpResponse))
20 /opt/lampp/htdocs/starlight-app/vendor/slim/slim/Slim/App.php(313): SlimApp->process(Object(SlimHttpRequest), Object(SlimHttpResponse))
21 /opt/lampp/htdocs/starlight-app/public/index.php(70): SlimApp->run()
22 {main}
БД настроена как виртуальный хост Что это значит. Виртуальный хост — это концепция Apache и не имеет ничего общего с СУБД.
— RiggsFolly
10.06.2019 13:24
Ответы
2
На момент вывода вашей ошибки он показывает некоторую проблему с разрешениями, вы можете предоставить полный доступ к корневой папке как sudo chmod 777
И убедитесь, что вы предоставляете действительные данные в теле API и передаете действительные данные json в ответ.
Вероятно, это могло бы решить проблему, но изменение разрешений для всей корневой папки вызвало у меня много проблем в прошлом проекте. В любом случае, спасибо.
— Triad
10.06.2019 13:33
Ответ принят как подходящий
Хорошо, извините за беспокойство, я просто решил проблему, исправив разрешения на запись и чтение в папку журналов, используя chmod -R a+wr logs/.
Спасибо.
Другие вопросы по теме
За последние 24 часа нас посетили 11528 программистов и 1149 роботов. Сейчас ищут 167 программистов …
-
- С нами с:
- 18 окт 2014
- Сообщения:
- 4
- Симпатии:
- 0
Slim Application Error
The application could not run because of the following error:Details
Type: PDOException
Code: 14
Message: SQLSTATE[HY000] [14] unable to open database file
File: /home/privetkakdela/www/site.com/example_site/library/ORM/idiorm.php
Line: 255
Trace#0 /home/privetkakdela/www/site.com/example_site/library/ORM/idiorm.php(255): PDO->__construct(‘sqlite:/home/pr…’, NULL, NULL, NULL)
#1 /home/privetkakdela/www/site.com/example_site/library/ORM/paris.php(105): ORM::_setup_db(‘default’)
#2 /home/privetkakdela/www/site.com/example_site/library/ORM/paris.php(325): ORMWrapper::for_table(‘c_catalog’, ‘default’)
#3 /home/privetkakdela/www/site.com/example_site/application/controllers.php(17): Model::factory(‘Catalog’)
#4 [internal function]: {closure}()
#5 /home/privetkakdela/www/site.com/example_site/library/Slim/Route.php(462): call_user_func_array(Object(Closure), Array)
#6 /home/privetkakdela/www/site.com/example_site/library/Slim/Slim.php(1326): SlimRoute->dispatch()
#7 /home/privetkakdela/www/site.com/example_site/library/Slim/Middleware/Flash.php(85): SlimSlim->call()
#8 /home/privetkakdela/www/site.com/example_site/library/Slim/Middleware/MethodOverride.php(92): SlimMiddlewareFlash->call()
#9 /home/privetkakdela/www/site.com/example_site/application/middlewares.php(8): SlimMiddlewareMethodOverride->call()
#10 /home/privetkakdela/www/site.com/example_site/library/Slim/Middleware/PrettyExceptions.php(67): RequestDataMiddleware->call()
#11 /home/privetkakdela/www/site.com/example_site/library/Slim/Slim.php(1271): SlimMiddlewarePrettyExceptions->call()
#12 /home/privetkakdela/www/site.com/example_site/application/app.php(16): SlimSlim->run()
#13 /home/privetkakdela/www/site.com/example_site/index.php(14): require(‘/home/privetkak…’)
#14 {main}
При открытии директории с сайтом появляется такая ошибка,помогите решить проблему желательно с обьяснениями, Ос убунту -
Команда форума
Модератор- С нами с:
- 15 мар 2007
- Сообщения:
- 9.901
- Симпатии:
- 968
раздать права на файл пользователю от которого работает процесс веб-сервера. вообще в гугле хренова туча рецептов на эту ошибку.
-
- С нами с:
- 18 окт 2014
- Сообщения:
- 4
- Симпатии:
- 0
Дал права командой sudo chmod -R 750 /home/privetkakdela/www/site.com
Теперь ForbiddenYou don’t have permission to access / on this server.
Apache/2.4.7 (Ubuntu) Server at site.com Port 80
-
Команда форума
Модератор- С нами с:
- 15 мар 2007
- Сообщения:
- 9.901
- Симпатии:
- 968
во-первых 0750 ибо лидирующая цифра за всякие липучки отвечает а юзер-группа-остальные уже потом описываются.
во-вторых у вас сайт принадлежит вам как пользователю и группе. веб-сервер выполняется от имени безопасных учетных записей. и тут вы ему хотите раздать 0 то есть запретить начиная от корня сайта. результат ваших необдуманных действий собственно уже налицо. а нужно было только файл базы скуэлайта раздать другому пользователю.
ну и в-третьих рекурсивно 0750 это та еще мартышка с гранатой. у вас там много файлов которые пользователь/группа должны выполнять как команды? думаю ни одного. -
- С нами с:
- 18 окт 2014
- Сообщения:
- 4
- Симпатии:
- 0
и как тперь это все исправить?
Добавлено спустя 6 минут 50 секунд:
Даже прописав sudo chmod -R 0750 /home/privetkakdela/www/site.com ничего не меняется -
Команда форума
Модератор- С нами с:
- 15 мар 2007
- Сообщения:
- 9.901
- Симпатии:
- 968
да раздайте вы 0755 каталогам и 0644 файлам и не парьтесь. вам еще рано углубляться в права доступа.
а у файла базы смените владельца на юзера от которого выполняется веб-процесс или права 0666 как вам удобнее. -
- С нами с:
- 18 окт 2014
- Сообщения:
- 4
- Симпатии:
- 0
получил страничку «Организации
О проекте
Web traningsАвторизация
Регистрация
Список КартаКаталог
Искусство и культура
«
хотя должен быть полноценный сайт с оформлением -
Команда форума
Модератор- С нами с:
- 15 мар 2007
- Сообщения:
- 9.901
- Симпатии:
- 968
скорее всего на какой-нибудь каталог кэша прав еще нужно раздать. ну сделайте 0777 на все каталоги и 0666 на все файлы. большая дыра но работать должно.
-
- С нами с:
- 7 июл 2014
- Сообщения:
- 330
- Симпатии:
- 0
Никогда не задавался этим вопросом, а что будет то собственно. В чем дыра?
У винды режим mode, так вообще в игноре, и что тут теперь одна сплошная дыра? Тут же везде 0777 и 0666 получается. -
Команда форума
Модератор- С нами с:
- 15 мар 2007
- Сообщения:
- 9.901
- Симпатии:
- 968
я создаю простой скрипт, иду в каталог к юзеру — 7 это чтение-запись-исполнение (в контексте каталога — переход). создаю там файлы какие хочу. правлю оригинальные файлы — 6 это чтение-запись ведь. таким образом каталог сайта в общем-то принадлежит ему, а пользуюсь им я.
модель безопасности каждый провайдер для себя сам выбирает. у меня mpm-itk, 0751 на каталоги и 0640 на файлы. это полностью защищает от листинга каталога соседа как в консоли так и от имени веб-сервера. в особых случаях права поднимаются — создается «демилитаризованная» зона с правами для нескольких пользователей объединенных в одну группу.
Решил опубликовать большой (!) лонгрид посвященный php-фреймворку Slim 4. Это микрофреймворк, а значит он содержит лишь самый минимум возможностей. С другой стороны Slim играет на поле full-stack фреймворков, таких как Symfony, Laravel и CodeIgniter.
Особенность Slim в том, что он по сути является лишь связующим звеном между сторонними библиотеками, которые созданы по стандартам PSR. Мы давно уже с этим сталкиваемся — сейчас доступно много хороших библиотек, которые можно использовать из коробки. Например для логов — Monolog, для контейнера — PHP-DI, для роутинга — FastRoute и т.д. Вот с помощью Slim можно всё это соединить воедино.
Но есть нюанс. Slim, хоть и небольшой фреймворк, но достаточно сложный. В основном мы привыкли работать в концепции MVC, где всё упирается в отдельные файлы контролёра, модели и шаблона, но вот Slim — это не MVC. Это ломает привычный подход и в процессе работы со Slim часто ловишь себя на мысли — зачем так усложнять, ведь можно сделать проще.
Поэтому прежде чем приступать к кодированию, нужно разобраться с основными концепциями, лежащими в основе фреймворка.
Исторически сложилось так, что у нас сейчас доминируют три фреймворка.
CodeIgniter можно считать классическим MVC-фреймворком. Мы можем закрыть глаза на некоторые его «особенности» в понимании Model, поэтому в целом его работа будет строиться следующим образом:
- Получить входящий URL.
- Взять из конфигурации список маршрутов (роутинг).
- Найти соответствующий текущему URL класс контролёра.
- Передать управление этому контролёру.
Входящий URL представляет собой т.н. сегменты (разделитель «/»). Отсюда и строится приложение — каждому сегменту будет соответствовать «контролёр/метод». Да, CodeIgniter, конечно же позволяет делать и другие варианты роутинга, но в целом используется именно такой подход. Он простой и понятный.
В противоположность ему можно поставить фреймворк Symfony. Это фреймворк оперирует понятиями http-«запрос-ответ» request/response. То есть на определённый http-запрос формируется http-ответ. И хотя по сути ответ формирует php-класс, который мы можем назвать контролёром, но на самом деле здесь больше подходит название Action (действие). Получается, что http-запрос вызывает какое-то действие, которое в свою очередь формирует http-ответ.
И уже не важно, будет ли этот Action выполнен как MVC или в любой другой структуре. Такой подход намного гибче «классического» MVC, поскольку очень много задач можно решать без Модели и Представления (view). Даже его автор определяет Symfony как HTTP-фреймворк. По современным представлениям это ближе к модели ADR (Action Domain Responder).
И казалось бы, давайте все перейдём на Symfony, если он такой классный. Но здесь «засада» — он слишком сложный. Например фреймворк очень активно использует аннотации. Например вместо файла с маршрутами роутинга — они прописываются в виде php-комментариев к нужному php-классу. Тоже самое для работы с базой данных. Для вывода испольузется тяжелый и неуклюжий шаблонизатор Twig. Кроме того для работы с Symfony используется консоль, с помощью которой генерируются php-файлы или выполняются какие-то действия. Получается, что фреймворк работает как «черный ящик» — всё слишком автоматизировано и понять что происходит достаточно проблематично.
Именно поэтому Symfony, хоть и имеет красивую теорию, годится только для больших команд и сложных проектов. Однако особо стоит отметить, что Symfony состоит из множества готовых и самостоятельных компонентов. Я думаю, что именно эти компоненты дали популярность фреймворку.
Фреймворк Laravel занимает некое промежуточное положение. Изначальный форк CodeIgniter, постепенно начал мигрировать в сторону HTTP-фреймворка (естественно под влиянием Symfony). Laravel начинает работать «как Symfony» — роутинг строится по принципу http-«запрос-ответ». В отличие от Symfony, здесь уже используется явное указание роутов: маршрут — callback-функция. Это намного удобней, чем искать аннотации.
После этого подключает контролёр (по сути Action) и дальше вывод строится по концепции MVC. При этом в качестве представления (View) используется шаблонизатор Blade либо «голый» php-файл. То есть Laravel придерживается достаточно жесткой структуры каталогов и файлов (как и CodeIgniter), но это упрощает создание приложения — достаточно знать где и что нужно менять и хранить.
Именно из-за этого Laravel более привлекателен для php-разработчиков — в нём разобраться намного проще.
С другой стороны Laravel — это full-stack фреймворк, а значит содержит много файлов. Начальное приложение занимает 36Мб на диске (из них FakerPHP — 10Мб). Таким образом использование Laravel оправдано только для больших и сложных сайтов. Дело не спасает и их «дочерний» фреймворк Lumen — он базируется абсолютно на тех же самых библиотеках, что и Laravel.
Кстати похожая ситуация была и с Silex — «дочерний» фрейморк Symfony. Несколько лет назад его закрыли.
Что получается в итоге? Когда-то давно фреймворки действительно были небольшими, но сейчас сильно «разжирели» (кроме CodeIgniter) и использовать их для небольшого сайта слишком затратно.
Вот здесь в игру и вступает Slim. Это HTTP-фреймворк, где роутинг — является основой приложения. Сам роутинг описывается простым «маршрут — callback-функция». В качестве роутинга используется библиотека FastRoute, что покрывает большинство потребностей.
Современное приложение использует контейнер зависимостей. Есть «родной» сервис-контейнер Slim, но он слишком уж примитивный, поэтому можно использовать PHP-DI.
Slim оперирует понятием Middleware — это т.н. промежуточный слой. Например мы хотим добавить к какому-то маршруту дополнительные данные или провести проверку. Вот именно в Middleware такие вещи и происходят.
Работа со Slim происходит примерно так:
- Создаём экземпляр приложения — по сути это объект роутинга.
- Создаём DI-контейнер, который будет хранилищем зависимостей.
- Если нужно формируем Middleware.
- Определяем маршруты — задаются правила роутинга.
- Запускаем приложение.
Slim после инициализации, создаст объекты Request и Response и передаст их в Action, который определен в роутинге. Если у этого маршрута определён Middleware, то он также будет выполнен через его обработчик (handle). На выходе будет сформирован (на основе созданного Response) http-ответ, который и будет отправлен браузеру.
Slim вообще никак не регламентирует где хранить конфигурацию, контролёры, логи и т.д., поэтому структура приложения может быть произвольной.
Но Slim заставляет достаточно жестко придерживаться стандартов PSR: основной это PSR-7 который указывает как именно работать с http-сообщениями. На практике такой подход выражается в строгой типизации параметров Action — на входе нужно принять запрос с типом ServerRequestInterface, ответ с типом ResponseInterface и на выходе отдать объект ResponseInterface. Именно это позволяет строить произвольные цепочки вызовов.
Поскольку сам по себе Slim небольшой, то для полноценного приложения нужно будет использовать Composer. Приятным бонусом будет его поддержка PSR-4 (автозагрузка классов). То есть в теории мы можем вообще всё сделать в одном index.php, а библиотеки будут в каталоге vendor. На практике, конечно же нам всё равно придётся писать свой код, поэтому есть смысл определиться с каталогами будущего приложения. Но это мы сделаем позже, вначале просто поставим Slim и запустим его на локальном сервере. Для этого нам нужен Composer.
Я буду устанавливать на свой локальный сервер http://demo/slim1 (потом slim2). Вы можете сделать аналогично, или используйте другой хост.
1. Переходим в каталог сайта и вызываем консоль. Запускаем команду, которая установит Slim
composer require slim/slim:"4.*"
Если Composer спросит «No composer.json in current directory, do you want to use the one at D:domainsdemo? [Y,n]?» выберите «n».
После установки должен появиться каталог vendor
и файлы composer.json
и composer.lock
.
2. Для работы Slim нужна какая-то PSR-7 библиотека. Будем использовать родную от Slim. Выполним:
composer require slim/psr7
3. Теперь здесь же создадим файл .htaccess
Options +SymLinksIfOwnerMatch Options -Indexes DirectoryIndex index.php AddDefaultCharset UTF-8 <IfModule mod_rewrite.c> RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^ index.php [QSA,L] </IfModule>
Это подключит ЧПУ.
4. Теперь сделаем index.php
<?php use PsrHttpMessageServerRequestInterface; use PsrHttpMessageResponseInterface; use SlimFactoryAppFactory; // подключаем Composer require 'vendor/autoload.php'; // создаём объект приложения $app = AppFactory::create(); // указываем базовый путь для роутинга — поскольку это подкаталог $app->setBasePath('/slim1'); // http://demo/slim1/ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response, $args) { $response->getBody()->write('Home page'); return $response; }); // http://demo/slim1/hello $app->get('/hello', function (ServerRequestInterface $request, ResponseInterface $response, $args) { $response->getBody()->write('Hello world!'); return $response; }); // запускаем приложение $app->run(); # end of file
5. Наберите в браузере http://demo/slim1/ и http://demo/slim1/hello — увидим соответствующее сообщение.
Обратите внимание на строчку с
setBasePath()
. Она нужна для случаев, если сайт располагается не в корне домена. Если у вас другой вариант, то измените эту строчку.
Давайте рассмотрим этот код.
Секция «use» показывает какие классы будут использоваться. Дальше мы подключаем автозагрузку файлов, который создал Composer.
Следующая строчка — это создание экземпляра приложения. Под капотом Slim использует FastRoute, но нигде этого указывать не нужно — вместо него используем оболочку AppFactory, которая хранит объекты роутинга, контейнера и т.д.
Маршруты указываются в виде http-методов GET, POST и т.п. Поэтому $app->get()
— это GET-запрос. В нём нужно указать шаблон URL и callback-функцию (Action). Эта функция принимает три параметра. Первый — это входящий запрос ServerRequestInterface, второй — это текущий ответ с типом ResponseInterface, а третий — дополнительные аргументы, которые могут быть в шаблоне URL.
Внутри action-функции будут доступны два объекта $request
и $response
. В нашем примере $response
формируется как тело ответа, куда мы отправляем простой текст. И в завершении функция возвращает объект $response
для того, чтобы обеспечить дальнейшее выполнение цепочки запросов.
Как только роуты будут определены, можно запустить приложение с помощью метода run()
.
Основы Middleware
Middleware переводят по разному. Это и посредники, и промежуточное ПО. С помощью Middleware можно внедрится в http-ответ. Причем можно это сделать на глобальном уровне, или локально — для какого-то одного маршрута.
Использовать ли Middleware зависит только от разработчика. Можно вполне спокойно обойтись и без них, а можно всё приложение сделать на Middleware. В Slim http-ответ формируется в Action, но вначале он проходит через слой Middleware, которые могут его изменить. В этом и есть их основное предназначение.
Конкретно в Slim 4 роутинг — это тоже Middleware, только на самом глубоком уровне. Поэтому вначале «срабатывает» слой Middleware, а потом слой роутинга.
Давайте для примера сделаем один Middleware. Это обычная функция, которая должна поддерживать интерфейс PSR-15.
Первый аргумент — это http-запрос с типом ServerRequestInterface. Второй аргумент — это handler-обработчик RequestHandlerInterface. На выходе функции должен быть ответ с типом ResponseInterface.
Я приведу весь код index.php
, чтобы вы не запутались:
<?php use PsrHttpMessageServerRequestInterface; use PsrHttpMessageResponseInterface; use SlimFactoryAppFactory; use PsrHttpServerRequestHandlerInterface; // обработчик Middleware // подключаем Composer require 'vendor/autoload.php'; // создаём объект приложения $app = AppFactory::create(); // указываем базовый путь для роутинга — поскольку это подкаталог $app->setBasePath('/slim1'); // создадим свой Middleware $myMiddleware = function (ServerRequestInterface $request, RequestHandlerInterface $handler) { $response = $handler->handle($request); $response->getBody()->write(' myMiddleware '); return $response; }; // добавим его в приложение $app->add($myMiddleware); // http://demo/slim1/ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response, $args) { $response->getBody()->write('Home page'); return $response; }); // http://demo/slim1/hello $app->get('/hello', function (ServerRequestInterface $request, ResponseInterface $response, $args) { $response->getBody()->write('Hello world!'); return $response; }); // запускаем приложение $app->run(); # end of file
Теперь, если обновить страницы в браузере, мы увидим приписку « myMiddleware ». Здесь мы добавили глобальный Middleware с помощью $app->add()
.
Попробуем добавить этот же Middleware только к одному маршруту.
Закомментируйте строчку с $app->add()
и добавьте его к маршруту:
// http://demo/slim1/ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response, $args) { $response->getBody()->write('Home page'); return $response; })->add($myMiddleware);
Теперь приписка « myMiddleware » будет срабатывать только для главной страницы сайта.
Дальше рассмотрим ещё одну возможность Slim — это отлов 404-страницы.
Для начала наберите в браузере несуществующий адрес, например http://demo/slim1/news. В браузере появится php-ошибка «Fatal error: Uncaught SlimExceptionHttpNotFoundException: Not found …». Чтобы от неё избавиться нужно добавить обработчик ErrorMiddleware, который будет перехватывать эту ошибку и выводить что-то более приемлемое.
$app->addErrorMiddleware(false, false, false);
Метод addErrorMiddleware()
принимает три обязательных параметра. Первый из них отвечает за вывод подробной информации об ошибке. Для этого его нужно указать как true
. Естественно на рабочем сайте этого не стоит делать.
Обработчик ErrorMiddleware можно заменить на другой вариант. Он пригодится для формирования своей 404-страницы. Такой код мы рассмотрим позже, когда будем строить своё приложение на основе классов.
Что будет если добавить несколько Middleware?
Сделайте копии $myMiddleware
как $myMiddleware1
и $myMiddleware2
и добавьте к приложению:
$app->add($myMiddleware1); $app->add($myMiddleware2);
После обновления страницы получится «Home page myMiddleware1 myMiddleware2». А теперь поменяйте порядок добавления:
$app->add($myMiddleware2); $app->add($myMiddleware1);
Получится «Home page myMiddleware2 myMiddleware1». Этот пример показывает важность порядка добавления Middleware.
Теперь давайте вернёмся к роутингу. Помимо фиксированного URL можно указывать определённый паттерн. Например если мы хотим перехватить все неизвестные адреса, то можем использовать именованный параметр для главной:
// http://demo/slim1/ $app->get('/', ... // http://demo/slim1/hello $app->get('/hello', ... // всё остальное $app->get('/{slug}', function (ServerRequestInterface $request, ResponseInterface $response, $args) { $response->getBody()->write('This page is ' . $args['slug']); return $response; });
В последнем роуте используется шаблон с подстановкой {slug}
, который автоматически будет хранится в параметре $args
. Это обычный массив, где будет ключ «slug». Мы его и выводим в браузере, если набрать любой несуществующий адрес. Такой приём позволяет перехватить 404-страницу без использования дополнительных Middleware.
При создании роутов нужно учитывать их порядок. Может случиться так, что маршруты будут перекрывать друг-друга, а значит Slim не сможет получить единственный обработчик и появится сообщение об ошибке «Slim Application Error». Для примера переместите последний роут в начало:
// всё остальное $app->get('/{slug}', ... // http://demo/slim1/ $app->get('/', ... // http://demo/slim1/hello $app->get('/hello', ...
Здесь мы получим ошибку, поскольку первый роут перекрывает остальные. Нужно за этим следить.
DI-контейнер
Ещё одной архитектурной концепцией Slim является работа с Dependency Container. Такой контейнер должен реализовывать PSR-11 и часто используется PHP-DI. Контейнер нужен для внедрения зависимостей. Я об этом уже рассказывал раньше. В простом виде это что-то вроде сервисов, которые будут доступны в разных частях приложения.
Контейнер привязывается к основному объекту приложения $app
, поэтому, если в каком-то другом классе нужно использовать контейнер, то его можно получить из $app
.
Для начала нам потребуется установить PHP-DI с помощью Composer. Наберите в консоли:
composer require php-di/php-di --with-all-dependencies
Ключ «—with-all-dependencies» нужен для того, что Slim и PHP-DI используют одни и те же PSR-интерфейсы, но в PHP-DI указана зависимость от более низкой версии.
Теперь подключаем его в index.php
. Я опять привожу полный код, чтобы никто не запутался:
<?php use PsrHttpMessageServerRequestInterface; use PsrHttpMessageResponseInterface; use SlimFactoryAppFactory; use DIContainer; // это и есть PHP-DI // подключаем Composer require 'vendor/autoload.php'; // Создание контейнера PHP-DI $container = new Container(); // Создаём объект приложения Slim и передаём в него контейнер AppFactory::setContainer($container); $app = AppFactory::create(); // указываем базовый путь для роутинга — поскольку это подкаталог $app->setBasePath('/slim1'); $app->addErrorMiddleware(false, false, false); // http://demo/slim1/ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response, $args) { $response->getBody()->write('Home page'); return $response; }); // http://demo/slim1/hello $app->get('/hello', function (ServerRequestInterface $request, ResponseInterface $response, $args) { $response->getBody()->write('Hello world!'); return $response; }); // запускаем приложение $app->run();
Важно здесь то, что контейнер нужно добавлять перед созданием приложения. Перед тем, как создавать $app
нужно передать ему контейнер через setContainer()
. Почему так? Обратите внимание, что AppFactory — это статические методы. Когда мы вызываем AppFactory::create()
, то по сути происходит инстанцирование объекта класса SlimApp
:
public static function create( ?ResponseFactoryInterface $responseFactory = null, ?ContainerInterface $container = null, ?CallableResolverInterface $callableResolver = null, ?RouteCollectorInterface $routeCollector = null, ?RouteResolverInterface $routeResolver = null, ?MiddlewareDispatcherInterface $middlewareDispatcher = null ): App { static::$responseFactory = $responseFactory ?? static::$responseFactory; return new App( self::determineResponseFactory(), $container ?? static::$container, $callableResolver ?? static::$callableResolver, $routeCollector ?? static::$routeCollector, $routeResolver ?? static::$routeResolver, $middlewareDispatcher ?? static::$middlewareDispatcher ); }
Поскольку AppFactory::create()
мы вызываем без параметров, то будут использованы переменные из текущего static-класса. В нашем случае static::$container
.
После этого контейнер можно получить из приложения:
$container = $app->getContainer();
Сейчас у нас нет зависимостей для контейнера, поэтому давайте добавим библиотеку для логирования Monolog в виде сервиса. Для этого установим её с помощью Composer:
composer require monolog/monolog
Теперь добавим в начало файла:
use MonologLogger; use MonologHandlerStreamHandler;
В после создания $app
добавим сервис logger
// Создадим объект для логирования через контейнер $container->set('logger', function () { $log = new Logger('general'); $log->pushHandler(new StreamHandler(__DIR__ . '/var/log/app.log', Logger::INFO)); return $log; });
Чтобы логи сохранялись создадим каталог «var/log» — в нём Monolog будет хранить файлы логов.
Добавим для примера логирование главной страницы:
// http://demo/slim1/ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response, $args) use ($app) { $container = $app->getContainer(); // если установлен лог, то пишем в него сообщение об ошибке if ($container->has('logger')) $container->get('logger')->info('Home'); $response->getBody()->write('Home page'); return $response; });
Чтобы получить доступ к $app
внутри функции, мы используем use
. На практике, скорее всего так делать не придётся, поскольку при работе с php-классами для роутинга, Slim автоматически будет передавать контейнер в конструктор. Это мы рассмотрим позже.
Сейчас главное понять общий принцип работы контейнера. Вначале нужно убедиться, что сервис logger существует, а потом его использовать как это определено в его классе Monolog.
Если вы посмотрите на файл var/log/app.log
, то увидите, что в нём стали появляться новые строчки.
Использование контейнера, а точнее его сервисов, делает приложение немного проще. После мы сможем выделить создание сервисов отдельным файлом — так будет удобней ими управлять.
Стоит отметить ещё один подход к проектированию приложения. Мы создали контейнер и потом, если нужно, получаем его из объекта $app
. Но можно поступить наоборот: создать контейнер, потом создать в нем сервис app и потом уже получить его из контейнера для работы. То есть главным становится уже контейнер.
Приведу полный рабочий код, чтобы было понятно:
<?php use PsrHttpMessageServerRequestInterface; use PsrHttpMessageResponseInterface; use SlimFactoryAppFactory; use DIContainer; use MonologLogger; use MonologHandlerStreamHandler; // подключаем Composer require 'vendor/autoload.php'; // Создание контейнера $container = new Container(); // создаем сервис app — основной объект приложения $container->set('app', function () { return AppFactory::create(); }); // получаем из контейнера и работаем как обычно $app = $container->get('app'); // указываем базовый путь для роутинга — поскольку это подкаталог $app->setBasePath('/slim1'); // отлавливаем ошибки $app->addErrorMiddleware(false, false, false); // Создадим объект для логирования через контейнер $container->set('logger', function () { $log = new Logger('general'); $log->pushHandler(new StreamHandler(__DIR__ . '/var/log/app.log', Logger::INFO)); return $log; }); // http://demo/slim1/ // обратите внимание, что здесь уже применяется use $container $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response, $args) use ($container) { // если установлен лог, то пишем в него сообщение об ошибке if ($container->has('logger')) $container->get('logger')->info('Home'); // если нужно, то можно получить $app из контейнера // $app = $container->get('app'); // var_dump($app->getContainer()); // null $response->getBody()->write('Home page'); return $response; }); // http://demo/slim1/hello $app->get('/hello', function (ServerRequestInterface $request, ResponseInterface $response, $args) { $response->getBody()->write('Hello world!'); return $response; }); // запускаем приложение $app->run(); # end of file
Поскольку $app
у нас хранится как сервис app, то мы можем получить его в любом месте. Но при этом $app
уже не управляет контейнером. Если выполнить:
$app = $container->get('app'); var_dump($app->getContainer());
То получим null
.
И хотя такой подход часто встречается, на мой взгляд всё-таки удобней пользоваться
$app
, в который внедрён контейнер. Тем более, что в Slim контейнер может автоматически передаваться в конструктор Action, а обработчик Middleware также может получить нужный$app
. Поэтому потребность прятать приложение в контейнер крайне мала.
И еще раз возвращаясь к логированию. Мы добавили лог к главной странице, явно прописав его в самом роуте. Как вы понимаете, для этого несколько удобней использовать Middleware.
// не забудьте добавить // use PsrHttpServerRequestHandlerInterface; ... // логирование всех запросов $logMiddleware = function (ServerRequestInterface $request, RequestHandlerInterface $handler) use ($app) { $container = $app->getContainer(); // если установлен лог, то пишем в него текущий url-путь if ($container->has('logger')) $container->get('logger')->info($request->getUri()->getPath()); $response = $handler->handle($request); return $response; }; // добавим его в приложение $app->add($logMiddleware); ...
Теперь вместо того, чтобы менять роуты, достаточно использовать $logMiddleware
.
Структура приложения
Теперь мы уже знакомы с основами Slim, поэтому можно расширить наше приложение. Пока у нас всё в одном файле index.php
, что не совсем правильно. Нам нужно придумать структуру каталогов, где мы будем хранить наши файлы. Тем более, что нам предстоит ещё разобраться как перейти на ОПП вместо функций роутинга и Middleware.
Если рассматривать типовые варианты именования каталогов, то обычно выделяют следующие.
- Каталог vendor — это каталог для Composer. Мы его не меняем и всегда используем его автозагрузку файлов (PSR-4).
- Каталог var — это каталог для изменяемых файлов. Это логи, кэш, может быть файлы SQLite.
- Каталог app — каталог пользовательского приложения. Именно здесь размещаются файлы конфигурации и прочих модулей.
- Каталог src — это каталог, где содержится «ядро» создаваемого приложения/фреймворка/CMS. Например если вы делаете публичный проект, то этот каталог может обновляться «накатом» — здесь не должно быть пользовательских файлов. Часто вместо src используют system/library/core и т.п.
- Каталог resources — предназначен для прочих файлов проекта.
Отдельно стоит отметить каталог public (иногда web). Обычно он указывает на web-каталог сервера public_html. То есть в этом каталоге размещаются только те файлы, доступ к которым возможен через браузер. Все остальные файлы размещаются на более высоком уровне сервера и недоступны извне. Такая схема хороша, но только если у вас не виртуальный хостинг.
Поскольку в 99% случаев сайты используют именно вирутальный хостинг, то каталог public не используется — всё располагается в public_html вашего сервера. Для закрытия доступа используется .htaccess с директивой Deny from all
, поэтому особых проблем с безопасностью не будет. Ну и плюс не стоит использовать открытые файлы, вроде .env
для конфигураций и хранения секретных данных. Вместо этого нужно использовать обычные php-файлы с массивами. Тогда даже прямой вызов файла никаких проблем не вызывает.
Поэтому мы будем строить так, чтобы приложение можно было использовать на виртуальном хостинге, но при желании, можно будет его перенести на любой уровень выше. Здесь вопрос чисто технический — достаточно определить базовый константы.
Что у итоге получается? Пусть у нас будет такая структура:
app/ Config/ Modules/ Middleware/ bootstrap.php src/ Core/ functions.php Debug/ Pr.php var/ cache/ log/ sqlite/ .htaccess index.php composer.json
Файл index.php
— фронт-контролёр. В его задачу входит создание базовых констант (это зависит от расположения файла) и передача управления в app/bootstrap.php
.
В файле bootstrap.php
формируется и запускается приложение Slim.
Дополнительные зависимости будут храниться в каталоге Config.
Каталог Modules будет хранить модули приложения. Мы говорили о концепции MVC и ADR, но вопрос лишь в том, как именно должны разделяться файлы одного модуля. Обычно принято хранить контролёры отдельно от моделей и представлений. Лично мне такой подход не нравится: я предпочитаю файлы модуля держать в одном месте. А какая там будет использоваться концепция — уже значения не имеет. Ну и кроме того, мы всего лишь знакомимся с Slim, поэтому большой разницы в этом разрезе не будет.
Каталог Middleware будет хранить классы Middleware.
Каталог src для хранения дополнительного функционала. Мы используем Composer, но часть кода придётся написать вручную. Поэтому здесь будут храниться функции и какие-то дополнительные классы.
Давайте начнём с чистого листа. Пусть у нас будет проект http://demo/slim2.
Вы можете сразу обратиться к итоговому коду, который я опубликовал на GitHub (github.com/maxsite/Slim-Skeleton). По ходу статьи я буду показывать откуда и что получилось.
Файл composer.json
будет таким:
{ "require": { "slim/slim": "4.*", "slim/psr7": "^1.5", "php-di/php-di": "^6.3", "monolog/monolog": "^2.3" }, "autoload": { "psr-4": { "App\": "app/", "Lib\": "src/" } } }
Здесь мы подключаем зависимости, а также определяем автозагрузку PSR-4. То есть namespace App будет ссылаться на каталог app, а Lib на src. Если вы захотите поменять расположение каталогов, то достаточно это будет сделать в этом файле и потом обновиться через Composer.
Теперь запускаем:
composer update
Появится каталог vendor. Зависимости установлены.
Файл index.php
сделаем таким:
<?php define('BASE_DIR', dirname(realpath(__FILE__)) . DIRECTORY_SEPARATOR); define('SLIM_APP_BASEPATH', '/' . basename(__DIR__)); require BASE_DIR . 'app/bootstrap.php'; # end of file
Здесь мы определяем каталог BASE_DIR
. Это нужно для того, чтобы зафиксировать корневой каталог приложения и все остальные файлы можно подключать относительно него. В теории можно ещё переопределить и app, var, но это уже экономия на спичках.
Константа SLIM_APP_BASEPATH
нам нужна для того, чтобы использовать в setBasePath()
. Раньше мы её определяли вручную, теперь используем на основе текущего каталога. Если у вас какое-то особое расположение сайта, то можно указать в этом файле, а не трогать bootstrap.php
.
Теперь давайте разбираться с bootstrap.php
. Мы уж знаем, что перед запуском приложения Slim необходимо выполнить несколько задач. Мы их выделим в отдельные файлы и сохраним в каталоге Config.
Первое что нужно выделить отдельно — это создание зависимостей для контейнера. Пусть это будет app/Config/container.php
, где мы разместим код создания логирования.
<?php use MonologLogger; use MonologHandlerStreamHandler; // Создадим объект для логирования через контейнер // https://github.com/Seldaek/monolog $container->set('logger', function () { $log = new Logger('general'); $log->pushHandler(new StreamHandler(BASE_DIR . 'var/log/app.log', Logger::INFO)); return $log; }); # end of file
После контейнера мы можем выделить какие-то настройки в app/Config/settings.php
. Пока мы оставим файл пустым, но в теории в нем можно будет хранить настройки всего приложения. А поскольку их можно будет держать в контейнере, то подключаем после container.php
.
Потом у нас пойдут Middleware в файле app/Config/middleware.php
. Пока он пусть будет пустым.
Также у нас будет ещё один особый Middleware для отлова ошибок (мы используем его для метода addErrorMiddleware). Пусть это будет файл app/Config/errorMiddleware.php
.
<?php $app->addErrorMiddleware(false, false, false); # end of file
После этого нам потребуется файл для задания маршрутов. Выделим для этого файл app/Config/routes.php
и разместим там наши адреса:
<?php use PsrHttpMessageServerRequestInterface; use PsrHttpMessageResponseInterface; // http://demo/slim2/ $app->get('/', function (ServerRequestInterface $request, ResponseInterface $response, $args) { $response->getBody()->write('Home page'); return $response; }); // http://demo/slim2/hello $app->get('/hello', function (ServerRequestInterface $request, ResponseInterface $response, $args) { $response->getBody()->write('Hello world!'); return $response; }); # end of file
Но это ещё не всё. В самом начале нам нужно подключить src/Core/functions.php
— это какие-то функции, которые могут использоваться на уровне всего приложения. Как правило всегда есть небольшие хелперы, которые использует разработчик. Например я использую функцию pr()
, которая есть в MaxSite CMS и Albireo для отладки кода (это обертка для print_r
).
Вот что у нас получилось в итоге:
<?php use SlimFactoryAppFactory; use DIContainer; require BASE_DIR . 'vendor/autoload.php'; require BASE_DIR . 'src/Core/functions.php'; $container = new Container(); AppFactory::setContainer($container); $app = AppFactory::create(); if (SLIM_APP_BASEPATH) $app->setBasePath(SLIM_APP_BASEPATH); require BASE_DIR . 'app/Config/container.php'; require BASE_DIR . 'app/Config/settings.php'; require BASE_DIR . 'app/Config/middleware.php'; require BASE_DIR . 'app/Config/errorMiddleware.php'; require BASE_DIR . 'app/Config/routes.php'; $app->run();
Попробуйте посмотреть сайт в браузере — всё должно работать. Теперь работать с приложением будет удобней.
Модули для роутинга. Отлов 404-ошибки
Мы пока работали с callback-функциями, а по хорошему нужно всё делать через php-классы. Для начала разберёмся с тем, как работать роутингом. Например мы хотим оформить главную страницу в виде обычного html-файла.
Для этого нам нужно привязать в роутинге адрес к определенному классу. Откроем файл routes.php
и вместо существующего кода для главной пропишем новый класс.
$app->get('/', 'AppModulesHomeHome:home');
Эту строчка указывает на то, чтобы Slim передал управление классу AppModulesHomeHome
с методом home()
. Поскольку у нас PSR-4, то это будет соответствовать файлу app/Modules/Home/Home.php
<?php /** * Вывод главной страницы */ namespace AppModulesHome; use PsrHttpMessageResponseInterface; use PsrHttpMessageServerRequestInterface; use PsrContainerContainerInterface; class Home { private $container; public function __construct(ContainerInterface $container) { $this->container = $container; } public function home(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { // простой шаблонизатор $data = ['name' => 'Вася']; $body = getTmpl(__DIR__ . '/home.template.php', $data); // формируем ответ $response->getBody()->write($body); return $response; } } # end of file
Рассмотрим этот код подробнее. Вначале указывается namespace, который совпадает с каталогом расположения файла. так работает PSR-4. Дальше секция use в которой перечисляются используемые типы данных.
Сам класс состоит из конструктора, который принимает контейнер приложения. Я об этом уже упоминал, поэтому если нужно, контейнер будет сразу доступен в нашем классе.
Метод home()
по сути и есть наша исходная callback-функция. Она имеет те же самые входные и выходные параметры. Но вместо простого текста, мы передаём в тело http-ответа содержимое файла шаблона home.template.php
:
<!DOCTYPE html> <html> <h1>Hello, <?= $name ?>!</h1> <h2>This is Slim4</h2> </html>
Для шаблонизатора используется core-функция getTmpl()
, которая принимает имя файла и массив данных. Массив будет автоматом развёрнут в обычные php-переменные: в нашем примере в шаблоне появится переменная $name
.
После обновления главная страница выведет:
Hello, Вася! This is Slim4
Подобный подход часто используется в MVC. Здесь $data
— это то, что будет получено из Модели, а $body
сформирован из View. Если вам нравится такой вариант, то вы легко сможете добавить нужные классы.
Очень часто класс контролёра MVC содержит сразу несколько методов. Например контролёр работы с формой может содержать кучу методов. Таким образом в роутинге нужно будет прописывать что-то вроде такого:
$app->get('/form', 'AppModulesFormController:show'); $app->post('/form', 'AppModulesFormController:create');
То есть здесь указывается один и тот же контролёр, но меняются его методы.
В качестве альтернативы можно использовать модель Action Domain Responder (ADR), где в основе лежит Action (действие). Action также представляет собой отдельный класс, но только с методом __invoke()
. Это магический php-метод, который позволяет использовать класс как функцию. Таким образом меняется и задание роутинга, и сам класс.
<?php /** * Вывод главной страницы */ namespace AppModulesHome; use PsrHttpMessageResponseInterface; use PsrHttpMessageServerRequestInterface; use PsrContainerContainerInterface; class Home { private $container; // constructor receives container instance public function __construct(ContainerInterface $container) { $this->container = $container; } public function __invoke(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { // простой шаблонизатор $data = ['name' => 'Вася']; $body = getTmpl(__DIR__ . '/home.template.php', $data); // формируем ответ $response->getBody()->write($body); return $response; } }
Роутинг же задается так:
$app->get('/', AppModulesHomeHome::class);
Результат будет точно таким же, но с __invoke()
можно не думать над именем метода.
Что касается ADR, то в ней принимается то, что Action должен описывать только одно единственное действие. В случае с «классическим» MVC, контролёр обычно содержит сразу много действий. Если следовать ADR, то каждый Action будет отдельным файлом. С одной стороны это увеличивает их количество, а с другой с ними легче работать. Кроме того, такие одиночные файлы (Action) могут использоваться повторно, поскольку не содержат зависимостей от других Action.
Какой вариант выбрать зависит только от вас. Slim не накладывает ограничений на структуру приложений.
Теперь давайте я покажу как можно перехватить 404-ошибку и вместо сообщения Slim выводить свой вариант, основанный на html-шаблоне.
Для начала откроем файл errorMiddleware.php
и сделаем его таким:
<?php use PsrHttpMessageServerRequestInterface; $customErrorHandler = function ( ServerRequestInterface $request, Throwable $exception ) use ($app) { return AppModulesPage404Page404::response($app, $exception, $request); }; if ($container->has('logger')) $errorMiddleware = $app->addErrorMiddleware(true, true, true, $container->get('logger')); else $errorMiddleware = $app->addErrorMiddleware(true, true, true); $errorMiddleware->setDefaultErrorHandler($customErrorHandler); # end of file
Здесь мы регистрируем для приложения свой обработчик ошибочных Middleware. Сам по себе обработчик будет возвращать наш класс AppModulesPage404Page404
. Метод response()
мы делаем статичным, чтобы упростить его вызов (но при желании можно сделать обычным классом). Также мы сразу передаём в приложение сервис logger, проверив что он есть.
Теперь делаем файл app/Modules/Page404/Page404.php
:
<?php /** * Модуль, который отвечает за вывод 404-ошибки */ namespace AppModulesPage404; use PsrHttpMessageServerRequestInterface; use FigHttpMessageStatusCodeInterface; class Page404 { public static function response($app, Throwable $exception, ServerRequestInterface $request) { $container = $app->getContainer(); // если установлен лог, то пишем в него сообщение об ошибке if ($container->has('logger')) $container->get('logger')->error('404', [$exception->getMessage()]); // простой шаблонизатор $body = getTmpl(__DIR__ . '/404.template.php'); // формируем ответ // указываем http-код ошибки 404 $response = $app->getResponseFactory()->createResponse(StatusCodeInterface::STATUS_NOT_FOUND); $response->getBody()->write($body); return $response; } } # end of file
Ну и сам html-шаблон в файле 404.template.php
:
<!DOCTYPE html> <html> <h1>404</h1> <h2>Not Found</h2> </html>
В принципе этот код уже нам знаком, обращу только момент на параметр метода createResponse()
, через который мы формируем http-ответ. Здесь указывается статус STATUS_NOT_FOUND
, что соответствует коду 404. Если этого не сделать, то Slim будет отдавать обычный код 200, что для ненайденной страницы неверно.
Метод AppModulesPage404Page404::response()
может быть совершенно произвольным. Главное, чтобы он возвращал response-объект.
Использование своего обработчика $errorMiddleware
один из способов отлова 404-страниц. Другой способ я уже показывал раньше — это указать последним роутер:
$app->get('/{slug}', AppModulesсвой-404-класс::class);
А потом создать этот класс по аналогии с Home.
Какой вариант использовать — зависит от разработчика.
Добавляем Middleware как PHP-классы
Как работать с Middleware мы уже знаем, поэтому рассмотрим вопрос как их переделать из функций в PHP-классы. У нас есть каталог app/Middleware, где будут храниться классы, а подключать их к приложению нужно будет в app/Config/middleware.php
В этом случае они будут работать для всех адресов.
Если же нужно прицепить Middleware к определённому маршруту, то делается это уже в файле роутинга app/Config/routes.php
Для начала создадим файл app/Middleware/AfterMiddleware.php
:
<?php /** * Пример Middleware */ namespace AppMiddleware; use PsrHttpMessageServerRequestInterface; use PsrHttpServerRequestHandlerInterface; use SlimPsr7Response; class AfterMiddleware { public function __invoke(ServerRequestInterface $request, RequestHandlerInterface $handler): Response { $response = $handler->handle($request); $response->getBody()->write('AFTER'); return $response; } } # end of file
Это пример из справки Slim. Здесь мы также используем __invoke()
, чтобы упростить вызов класса. Теперь добавим в файл app/Config/middleware.php
$app->add(AppMiddlewareAfterMiddleware::class);
Теперь на каждой странице будет текстовая приписка «AFTER». Если же нужно добавить Middleware для конкретного маршрута, то указываем это в его роутинге:
$app->get('/', AppModulesHomeHome::class)->add(AppMiddlewareAfterMiddleware::class);
По такому принципу строятся все Middleware. Но мы рассмотрим ещё один пример из справки Slim — класс BeforeMiddleware:
<?php /** * Пример Middleware */ namespace AppMiddleware; use PsrHttpMessageServerRequestInterface; use PsrHttpServerRequestHandlerInterface; use SlimPsr7Response; use PsrContainerContainerInterface; class BeforeMiddleware { private $container; public function __construct(ContainerInterface $container) { $this->container = $container; } public function __invoke(ServerRequestInterface $request, RequestHandlerInterface $handler): Response { $response = $handler->handle($request); $existingContent = (string) $response->getBody(); $response = new Response(); $response->getBody()->write('BEFORE' . $existingContent); return $response; } } # end of file
Здесь я добавил конструктор класса, через который можно получить контейнер приложения. Например если нужен сервис logger, то мы берём его из контейнера.
Теперь обратите внимание, как формируется ответ. Вначале получается текущее тело запроса, а потом создаётся новый объект, в который отправляется изменённый текст. Суть в том, что ответ $response
можно получить как из handler-обработчика, так и создать самостоятельно новый.
Настройки Settings
Наше приложение уже почти готово, осталось добавить только settings-настройки. Где они могут пригодится? Например можно хранить управление выводом ошибок для addErrorMiddleware()
. Скажем для отладки было бы полезным получить полный и красивый отчёт вывода. Логично эти опции вынести в отдельный файл.
Есть несколько подходов к организации настроек сайта, но наиболее привычным способом будет использование в конфигурационном файле обычного php-массива, который возвращается по return. Например файл app/Config/app.php
:
return [ 'ErrorMiddleware' => [ 'customErrorHandler' => false, // свой обработчик errorMiddleware 'displayErrorDetails' => true, // выводить детали ошибки 'logErrors' => false, // писать в лог 'logErrorDetails' => false, // детали в лог ], ];
Но нюанс в том, что эти данные нам могут понадобиться в разных частях программы, поэтому правильным было бы перенести их в общий контейнер. Второй момент — может получиться так, что нужно будет работать с разными конфигурационными файлами, работающими по этому же принципу (возврат по return). Например, если нужно будет добавить конфигурационный файл для баз данных db.php
. Поэтому нужен механизм, позволяющий работать с разными файлами.
Для этого мы будем использовать простой класс LibSettingsSettings
, который будет храниться в контейнере, но перед этим он считает все указанные конфигурационные файлы из каталога app/Config
.
Таким образом файл app/Config/settings.php
получится таким:
<?php // alias => файл конфигурации $files = [ 'app' => 'app.php', 'db' => 'db.php', ]; // сохраняем в контейнере $container->set('config', function () use ($files) { return new LibSettingsSettings($files); }); # end of file
В массиве $files
мы указываем имя файла и его псевдоним/ключ. Дальше сохраняем в контейнере класс Settings. Он в свою очередь сам считает все указанные файлы.
<?php namespace LibSettings; class Settings { private $settings = []; public function __construct(array $files) { $pathConfig = BASE_DIR . 'app/Config/'; foreach ($files as $alias => $file) { if (file_exists($pathConfig . $file)) { $this->settings[$alias] = require $pathConfig . $file; } } } public function getAlias(string $alias, $default = []) { return $this->settings[$alias] ?? $default; } } # end of file
Для получения массива из файла конфигурации, вначале нужно получить экземпляр объекта Settings из контейнера, а потом выполнить метод getAlias()
.
$confApp = $container->get('config')->getAlias('app'); pr($confApp); Array ( [ErrorMiddleware] => Array ( [customErrorHandler] => [displayErrorDetails] => 1 [logErrors] => [logErrorDetails] => ) )
Для примера используем эту конфигурацию в errorMiddleware.php
. Я добавил немного логики, чтобы более гибко управлять 404-ошибками.
<?php use PsrHttpMessageServerRequestInterface; // получаем данные из контейнера $confApp = $container->get('config')->getAlias('app'); // пошли опции $customErrorHandler = $confApp['ErrorMiddleware']['customErrorHandler'] ?? true; $displayErrorDetails = $confApp['ErrorMiddleware']['displayErrorDetails'] ?? false; $logErrors = $confApp['ErrorMiddleware']['logErrors'] ?? false; $logErrorDetails = $confApp['ErrorMiddleware']['logErrorDetails'] ?? false; if ($customErrorHandler) { // Define Custom Error Handler $customErrorHandler = function ( ServerRequestInterface $request, Throwable $exception ) use ($app) { return AppModulesPage404Page404::response($app, $exception, $request); }; // Add Error Middleware if ($container->has('logger')) $errorMiddleware = $app->addErrorMiddleware($displayErrorDetails, $logErrors, $logErrorDetails, $container->get('logger')); else $errorMiddleware = $app->addErrorMiddleware($displayErrorDetails, $logErrors, $logErrorDetails); $errorMiddleware->setDefaultErrorHandler($customErrorHandler); } else { // Slim вариант вывода ошибок if ($container->has('logger')) $app->addErrorMiddleware($displayErrorDetails, $logErrors, $logErrorDetails, $container->get('logger')); else $app->addErrorMiddleware($displayErrorDetails, $logErrors, $logErrorDetails); } # end of file
Дальше просто. Если нужно добавить новый конфигурационный файл, то достаточно его указать в app/Config/settings.php
. После он автоматом станет доступным через контейнер.
Работа с модулями приложения
В целом каркас приложения уже готов, но мы добавим в него ещё несколько штрихов, чтобы сделать работу с модулями более удобной. Лично я привык именно к модульной структуре, когда все файлы модуля расположены в одном месте. С практической точки зрения это намного удобней, чем раскидывать файлы по разным частям приложения.
Работать с такими модулями удобно ещё и потому что они по сути автономны. Если модуль не нужен, то достаточно его просто удалить. Но вот чтобы подключить модуль нам нужно предусмотреть механизм, который будет автоматически подключать файлы модуля к основному приложению. Например модуль имеет свой роутинг. Если он фиксированный, то было бы здорово сделать так, чтобы он автоматом подключался к основному routes.php
.
C другой стороны у модуля могут быть и другие задачи, например работа с контейнером или регистрация своих php-классов. То есть мы не знаем наверняка что именно будет делать модуль, но он должен получить доступ к $app
до его запуска.
Поэтому поступим хитро. Подключим в bootstrap.php
файл app/Config/initModules.php
... require BASE_DIR . 'app/Config/errorMiddleware.php'; require BASE_DIR . 'app/Config/initModules.php'; require BASE_DIR . 'app/Config/routes.php'; ...
Сам же initModules.php
будет автоматом искать файлы init.php
в каждом модуле и подключать как обычно.
<?php $allFiles = glob(BASE_DIR . 'app/Modules/*/init.php'); foreach ($allFiles as $file) { require $file; } # end of file
Таким образом, у разработчика модуля будет возможность с помощью файла init.php
добавить свои роуты.
Для примера сделаем модуль Form, где рассмотрим работу с обычной формой. Создадим каталог app/Modules/Form
и в нём несколько файлов.
Роутинг пропишем в init.php
// http://demo/slim2/form $app->get('/form', AppModulesFormFormShow::class); $app->post('/form', AppModulesFormFormPost::class);
Форма будет иметь два состояния: отображение самой формы и приём данных. Отображение формы будет формироваться в FormShow.php
. Приём будет идти в FormPost.php
с одноименными классами. Я буду следовать не MVC, а ADR, поэтому в классах буду использовать метод __invoke()
.
В качестве файла с html-кодом формы будем использовать show.template.php
:
<!DOCTYPE html> <html> <h1>Sample form</h1> <?= $message ?> <div> <form method="POST"> <div><label>Name: <input type="text" name="name"></label></div> <div><label>Email: <input type="text" name="email"></label></div> <div><button type="submit">Send</button></div> </form> </div> </html>
Поскольку это демо-пример, то разметка примитивная.
Теперь сделаем FormShow.php
, который будет просто выводить эту форму:
<?php /** * Вывод формы */ namespace AppModulesForm; use PsrHttpMessageResponseInterface; use PsrHttpMessageServerRequestInterface; class FormShow { public function __invoke(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { $body = getTmpl(__DIR__ . '/show.template.php', ['message' => '']); $response->getBody()->write($body); return $response; } } # end of file
Переменная $message
будет содержать результат отправки формы. При начальном отображении там пусто.
Блок отображения также сделаем в виде шаблона message.template.php
.
<div> <div><b>Name:</b> <?= htmlspecialchars($name) ?></div> <div><b>Email:</b> <?= htmlspecialchars($email) ?></div> </div>
В нём будут выводиться данные формы.
Теперь класс обработчика POST-запроса. Файл FormPost.php
<?php /** * Получение post от формы */ namespace AppModulesForm; use PsrHttpMessageResponseInterface; use PsrHttpMessageServerRequestInterface; class FormPost { public function __invoke(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface { // получить данные из формы $parsedBody = $request->getParsedBody(); $message = getTmpl(__DIR__ . '/message.template.php', [ 'name' => $parsedBody['name'], 'email' => $parsedBody['email'], ]); $body = getTmpl(__DIR__ . '/show.template.php', ['message' => $message]); $response->getBody()->write($body); return $response; } } # end of file
Данные от формы получаем в запросе через метод getParsedBody()
. На выходе обычный массив, как если бы мы работало через $_POST
. Дальше формируем блок сообщения $message
с помощью шаблона message.template.php
. В него мы и передаём данные формы. А уже после ещё раз выводим форму, где и выводится блок message.
Логика примера достаточно простая, я специально не стал её усложнять, чтобы можно было легко разобраться. На практике же нужно ещё добавить валидацию входящих данных, потом какие-то действие, поскольку форма нужна для каких-то задач (например отправка на email).
Несколько слов о шаблонах.
Сейчас это очень простые файлы, которые принимают готовые php-переменные. По этому принципу работают практически все существующие шаблонизаторы. Разница только в «синтаксическом сахаре» и дополнительных функциях. Поэтому не важно какой именно будет шаблонизатор использоваться в вашем проекте, суть будет одна — указать имя шаблона и массив данных.
Но есть ещё один момент, на который нужно обратить внимание. Представьте себе, что вы создаёте «движок», а не готовый сайт. Поэтому html-разметка будет отличаться от вашего исходного варианта. Почти все современные php-фреймворки рассчитаны только на одну разметку, поэтому у конечного пользователя нет другой возможности, кроме как менять шаблон в самом модуле или каком-то выделенном каталоге (например templates). То есть вопрос, что фреймворк будет поддерживать шаблоны сайта, как это сделано в любой CMS, обычно разработчиками не ставится.
Сложность здесь в том, что файл шаблона (например show.template.php
) имеет довольно жесткую привязку заданному каталогу. Таким образом, единственный правильный способ — это сделать так, чтобы модуль брал сам на себя проверку пользовательского файла шаблона. Примерно так:
... $fileTmpl = '/show.template.php'; if (file_exists(BASE_DIR . 'resources/views/Modules/Form' . $fileTmpl)) $file = BASE_DIR . 'resources/views/Modules/Form' . $fileTmpl; else $file = __DIR__ . $fileTmpl; $body = getTmpl($file, ['message' => '']); ...
Либо можно воспользоваться небольшой функцией-хелпером findFileTmpl()
, которая возвращает имя файла либо в пользовательском каталоге (resources/views/Modules/Form), либо в каталоге модуля (app/Modules/Form).
... $file = findFileViews('Modules/Form', 'show.template.php'); // каталог модуля, файл $body = getTmpl($file, ['message' => '']); ...
То есть модуль позволяет разместить файл шаблона в любом другом предопределенном каталоге. Если «движок» предполагает разные шаблоны сайта (как в CMS), то выбранный каталог может указываться в общей конфигурации. Таким образом, если у пользователя возникнет задача изменить вёрстку формы, то ему не нужно будет переписывать файлы модуля.
Ну и плюс такого подхода в том, что мы можем размещать файлы абсолютно произвольно, что редко встречается в других php-фрейморках.
В любом случае вопрос о шаблонизации сайта несколько выходит за рамки статьи, и этот вопрос отдан на откуп разработчику.
Итого
Slim 4 хорош для проектов, где использование больших фреймворков не оправдано. Главная «фишка» Slim — это его поддержка стандартов PSR, что позволяет без особых сложностей подключать и использовать сторониe PSR-библиотеки к своему приложению.
Итоговый код вы можете скачать с моего репозитория на GitHub: github.com/maxsite/Slim-Skeleton.