Skip to content

Instantly share code, notes, and snippets.

@odan
Last active January 15, 2022 11:22
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save odan/c8bee474b0054a06776481a6c8de1d8f to your computer and use it in GitHub Desktop.
Save odan/c8bee474b0054a06776481a6c8de1d8f to your computer and use it in GitHub Desktop.
Slim 4 Tutorial
@odan
Copy link
Author

odan commented Mar 5, 2020

Hi @thallestorma Your repository has to define the required connection class within the constructor parameter list (dependency injection).

Read more: https://odan.github.io/2019/12/03/slim4-eloquent.html

@thallestorma
Copy link

thallestorma commented Mar 6, 2020

Hi @odan

Thanks for your reply.

I got this error:

{
    "statusCode": 500,
    "error": {
        "type": "SERVER_ERROR",
        "description": "Argument 2 passed to App\\Domain\\Account\\Repository\\AccountListRepository::__construct() must be an instance of App\\Database\\SecondConnection, instance of Illuminate\\Database\\MySqlConnection given, called in \/home\/thallestorma\/MyProjects\/RuffusMuApi\/vendor\/php-di\/php-di\/src\/Definition\/Resolver\/ObjectCreator.php on line 142"
    }
}

dependencies.php:

// ...
use Illuminate\Container\Container as IlluminateContainer;
use Illuminate\Database\Connection;
use Illuminate\Database\Connectors\ConnectionFactory;
use Psr\Container\ContainerInterface;
use App\Database\SecondConnection;

// ...

Connection::class => function (ContainerInterface $container) {
    $factory = new ConnectionFactory(new IlluminateContainer());

    $connection = $factory->make($container->get('settings')['mu']);

    // Disable the query log to prevent memory issues
    $connection->disableQueryLog();

    return $connection;
},
SecondConnection::class => function (ContainerInterface $container) {
    $factory = new ConnectionFactory(new IlluminateContainer());

    $connection = $factory->make($container->get('settings')['rff']);

    // Disable the query log to prevent memory issues
    $connection->disableQueryLog();

    return $connection;
},
PDO::class => function (ContainerInterface $container) {
    return $container->get(Connection::class)->getPdo();
},
// ...

Connection have SQL Server settings and SecondConnection have MySQL settings.

Any suggestions?

Thanks

@odan
Copy link
Author

odan commented Mar 6, 2020

Hi @thallestorma I just fixed the example:

https://odan.github.io/2019/12/03/slim4-eloquent.html#setup-multiple-connections

The resolver for the second connection was missing.

Connection::resolverFor('mysql2', function ($connection, $database, $prefix, $config) {
    return new SecondConnection($connection, $database, $prefix, $config);
});

Here is the full diff:

odan/odan.github.io@897eb59#diff-d7c0123848050331815f231316659b73

@thallestorma
Copy link

Hi @odan

It worked like a charm.

Thank you

@onelostpuppy
Copy link

onelostpuppy commented Mar 18, 2020

Is there a way to use the same PDO connection for all routes? I think i am running into an issue with max connections in mysql. I am creating an android app and each device makes multiple api calls to multiple routes. Is it possible to re use the connection between the API and mysql for every API call from clients?

@odan
Copy link
Author

odan commented Mar 19, 2020

Hi @onelostpuppy If you use the DI container and dependency injection, all your objects should have the same instance (connection) per request. If you get the error max connections, you might create a new PDO instance for each SQL query, which is not correct. Better use the container to keep a shared PDO instance.

@onelostpuppy
Copy link

Ah thank you, I had made a mistake in a route and it was not using the container correctly it was creating new connections. Thank you for your time.

@glewe
Copy link

glewe commented Apr 13, 2020

Hi and thank you.
I followed the docuemntation up to "PSR-4 autoloading" before which I should able to pull the Hello World page in my browser. Except, I am getting this error.

Type: TypeError
Code: 0
Message: Argument 1 passed to Closure::{closure}() must be an instance of ServerRequestInterface, instance of Slim\Psr7\Request given, called in D:\Web\htdocs\lewe\slim4tut\vendor\slim\slim\Slim\Handlers\Strategies\RequestResponse.php on line 43
File: D:\Web\htdocs\lewe\slim4tut\config\routes.php
Line: 7

My website root is "D:\Web\htdocs". I set $app->setBasePath('/lewe/slim4tut'); That seems to work since everything else throws a 404.

I also noticed that Composer is always complaining this:
Package jeremeamia/superclosure is abandoned, you should avoid using it. Use opis/closure instead.

Does that have to do with it?

@odan
Copy link
Author

odan commented Apr 13, 2020

Hi @glewe

must be an instance of ServerRequestInterface, instance of Slim\Psr7\Request

Check for a missing use Psr\Http\Message\ServerRequestInterface; statement in your Action class.

Package jeremeamia/superclosure is abandoned, you should avoid using it. Use opis/closure instead.

The component jeremeamia/superclosure is a sub-dependency from PHP-DI. Related:

I hope this will be fixed soon.

@sunildabhi
Copy link

Hi @odan, nice tutorial thanks for it

can you give me example of basic authentication, i only want to check apikey passed in header which is mandatory to pass in header of each API call.

so please guide me how to implement in middleware of where i can check apikey in database every time.

@odan
Copy link
Author

odan commented Apr 17, 2020

Hi @sunildabhi

For BasicAuth you can try: tuupola/slim-basic-auth
For JWT without public/private key managment you may try this: tuupola/slim-jwt-auth
For OAuth 2.0 and JSON Web Token with a public/private key you can read this: https://odan.github.io/2019/12/02/slim4-oauth2-jwt.html

@francoisnza
Copy link

Great tutorial, thank you!

@Cvar1984
Copy link

Cvar1984 commented Apr 23, 2020

Do you have tutorial with slim/php-view or twig?

@odan
Copy link
Author

odan commented Apr 23, 2020

@sunildabhi
Copy link

hello @odan can you give me one example of user listing, all working fine but i want to know how to pass UserData array with return type and use it in foreach loop with UserData type. so if possible provide one example of listing API. thanks in advance :)

@fvtorres
Copy link

Hey @odan

I went though your Tutorial, the Twig implementation and the Mailer Implementation.

So good so far, but when I tried to use Twig inside emails, it simply doenst work. I was following the official documentation, but I get an error:
Type: Symfony\Component\Mime\Exception\LogicException
Code: 0
Message: A message must have a text or an HTML part or attachments.
File: /vendor/symfony/mime/Email.php
Line: 405

I found a reference in this github:
symfony/symfony#35990

If this is easy to solve, I guess it would be nice to be in your tutorials

@odan
Copy link
Author

odan commented Apr 30, 2020

@fvtorres

For technical questions please create an issue here:

https://github.com/odan/slim4-skeleton/issues

@fvtorres
Copy link

fvtorres commented Apr 30, 2020 via email

@juanma-mol
Copy link

Great work Odan!!
Thanks a lot for tutorial.
I try to catch PDOException from Insert User and can't figured out how to do
I read https://odan.github.io/2020/05/27/slim4-error-handling.html but no help

On my routes.php:
$app->post('/users/add', "\App\Action\UsersAction:addUser");

UsersAction.php:

<?php

namespace App\Action;
use App\Controller\Users;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use App\Factory\LoggerFactory;
use Psr\Log\LoggerInterface;
final class UsersAction
{
    private $userController;
    private $logger;
    public function __construct(Users $Users, LoggerFactory $loggerFactory)
    {
        $this->userController = $Users;
        $this->logger = $loggerFactory
        ->addFileHandler('users.log')
        ->createInstance('user_creator');
    }
    public function addUser(ServerRequestInterface $request, ResponseInterface $response ): ResponseInterface
    {
        $parsedBody = (array)$request->getParsedBody();

        $this->logger->info("addUser:".json_encode($parsedBody));

        $result = $this->userController->createUser($parsedBody);

        $response->getBody()->write((string)json_encode($result));
        return $response
            ->withHeader('Content-Type', 'application/json')
            ->withStatus(200);
    }
}

and UsersRepository:

<?php

namespace App\Domain\User\Repository;

use PDO;
use App\Exception\RepositoryException;
use App\Exception\ValidationException;

/**
 * Repository.
 */
class UsersRepository
{
    /**
     * @var PDO The database connection
     */
    private $connection;

    /**
     * Constructor.
     *
     * @param PDO $connection The database connection
     */
    public function __construct(PDO $connection)
    {
        $this->connection = $connection;
    }

    /**
     * Insert user row.
     *
     * @param array $user The user
     *
     * @return int The new ID
     */
    public function insertUser(array $user): int
    {
        $encriptedPassword = password_hash(@$user['password'], PASSWORD_BCRYPT,array('cost'=>12));
        $row = [
            'username' => @$user['username'],
            'name' => @$user['name'],
            'surname_1' => @$user['surname_1'],
            'surname_2' => @$user['surname_2'],
            'email' => @$user['email'],
            'password' => $encriptedPassword,
        ];

        $sql = "INSERT INTO users SET 
                username=:username, 
                name=:name, 
                surname_1=:surname_1, 
                surname_2=:surname_2, 
                email=:email,
                password=:password;";
        $this->connection->prepare($sql)->execute($row);

        return (int)$this->connection->lastInsertId();
    }
}

I want to return json with PDO error.
I try to create a midleware PDOExecption, try a try/catch on "$this->connection->prepare($sql)->execute($row);"

@odan
Copy link
Author

odan commented Aug 10, 2020

Hi @juanma-mol First thing is, the Action class is already a "controller", so calling a controller from an action makes no sense. An Action invokes a Service and a Service (business logic) invokes a Repository (data access logic). Now to your question...

To transform all PDOExecption into a JSON response, you can create a Middleware, (e.g. DatabaseExceptionMiddleware) that catches all database specific exceptions and transforms the exception into an JSON response.

So your middleware could look like this:

<?php

namespace App\Middleware;

use PDOException;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class DatabaseExceptionMiddleware implements MiddlewareInterface
{
    private $responseFactory;

    public function __construct(ResponseFactoryInterface $responseFactory)
    {
        $this->responseFactory = $responseFactory;
    }

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        try {
            return $handler->handle($request);
        } catch (PDOException $exception) {
            // Transform exception to JSON
            $result = [
                'error' => [
                    'code' => $exception->getCode(),
                    'message' => $exception->getMessage(),
                ]
            ];

            $response = $this->responseFactory->createResponse(500);
            $response->getBody()->write((string)json_encode($result));

            return $response;
        }
    }
}

Add this new middleware before the Slim ErrorMiddleware.

use \App\Middleware\DatabaseExceptionMiddleware;
// ...

$app->add(\App\Middleware\DatabaseExceptionMiddleware::class);
$app->addErrorMiddleware();

@juanma-mol
Copy link

Hi, thaks for answer my question.
(I am a former programmer, who switched to sysadmin in the 90's and now I want to learn programming again)
I try your code and get error:

Type: DI\Definition\Exception\InvalidDefinition
Code: 0
Message: Entry "App\Middleware\DatabaseExceptionMiddleware" cannot be resolved: Entry "Psr\Http\Message\ResponseFactoryInterface" cannot be resolved: the class is not instantiable Full definition: Object ( class = #NOT INSTANTIABLE# Psr\Http\Message\ResponseFactoryInterface lazy = false ) Full definition: Object ( class = App\Middleware\DatabaseExceptionMiddleware lazy = false __construct( $responseFactory = get(Psr\Http\Message\ResponseFactoryInterface) ) )
File: /var/www/html_v2/api4/vendor/php-di/php-di/src/Definition/Exception/InvalidDefinition.php
Line: 18
Trace
#0 /var/www/html_v2/api4/vendor/php-di/php-di/src/Definition/Resolver/ObjectCreator.php(155): DI\Definition\Exception\InvalidDefinition::create(Object(DI\Definition\ObjectDefinition), 'Entry "App\\Midd...')
#1 /var/www/html_v2/api4/vendor/php-di/php-di/src/Definition/Resolver/ObjectCreator.php(71): DI\Definition\Resolver\ObjectCreator->createInstance(Object(DI\Definition\ObjectDefinition), Array)
#2 /var/www/html_v2/api4/vendor/php-di/php-di/src/Definition/Resolver/ResolverDispatcher.php(64): DI\Definition\Resolver\ObjectCreator->resolve(Object(DI\Definition\ObjectDefinition), Array)
#3 /var/www/html_v2/api4/vendor/php-di/php-di/src/Container.php(387): DI\Definition\Resolver\ResolverDispatcher->resolve(Object(DI\Definition\ObjectDefinition), Array)
#4 /var/www/html_v2/api4/vendor/php-di/php-di/src/Container.php(138): DI\Container->resolveDefinition(Object(DI\Definition\ObjectDefinition))
#5 /var/www/html_v2/api4/vendor/slim/slim/Slim/CallableResolver.php(144): DI\Container->get('App\\Middleware\\...')
#6 /var/www/html_v2/api4/vendor/slim/slim/Slim/CallableResolver.php(101): Slim\CallableResolver->resolveSlimNotation('App\\Middleware\\...')
#7 /var/www/html_v2/api4/vendor/slim/slim/Slim/CallableResolver.php(79): Slim\CallableResolver->resolveByPredicate('App\\Middleware\\...', Array, 'process')
#8 /var/www/html_v2/api4/vendor/slim/slim/Slim/MiddlewareDispatcher.php(187): Slim\CallableResolver->resolveMiddleware('App\\Middleware\\...')
#9 /var/www/html_v2/api4/vendor/slim/slim/Slim/Middleware/ErrorMiddleware.php(107): class@anonymous->handle(Object(Slim\Psr7\Request))
#10 /var/www/html_v2/api4/vendor/slim/slim/Slim/MiddlewareDispatcher.php(188): Slim\Middleware\ErrorMiddleware->process(Object(Slim\Psr7\Request), Object(class@anonymous))
#11 /var/www/html_v2/api4/vendor/slim/slim/Slim/MiddlewareDispatcher.php(81): class@anonymous->handle(Object(Slim\Psr7\Request))
#12 /var/www/html_v2/api4/vendor/slim/slim/Slim/App.php(215): Slim\MiddlewareDispatcher->handle(Object(Slim\Psr7\Request))
#13 /var/www/html_v2/api4/vendor/slim/slim/Slim/App.php(199): Slim\App->handle(Object(Slim\Psr7\Request))
#14 /var/www/html_v2/api4/public/index.php(7): Slim\App->run()
#15 {main}

My code looks like:

<?php

namespace App\Middleware;

use PDOException;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
// use App\Factory\LoggerFactory;
// use Psr\Log\LoggerInterface;

/**
 * Middleware.
 */

final class DatabaseExceptionMiddleware implements MiddlewareInterface
{
    /**
     * @var LoggerInterface
     */
    private $logger;

    private $responseFactory;

    /**
     * The constructor.
     *
     * @param LoggerFactory $loggerFactory The logger
     */

    //public function __construct(ResponseFactoryInterface $responseFactory, LoggerFactory $loggerFactory)
    public function __construct(ResponseFactoryInterface $responseFactory)
    {
        $this->responseFactory = $responseFactory;
        // $this->logger = $loggerFactory
        //     ->addFileHandler('errors.log')
        //     ->createInstance('error_handler_middleware');
    }

    /**
     * Invoke middleware.
     *
     * @param ServerRequestInterface $request The request
     * @param RequestHandlerInterface $handler The handler
     *
     * @return ResponseInterface The response
     */
    public function process(
        ServerRequestInterface $request, 
        RequestHandlerInterface $handler
    ): ResponseInterface
    {
        try {
            return $handler->handle($request);
        } catch (PDOException $pdoException) {
            //$this->logger->error("pdoException");

            $result = [
                'error' => [
                    'code' => $pdoException->getCode(),
                    'message' => $pdoException->getMessage(),
                ]
            ];
            // $response = (new Response())->withStatus(404);
            // $response->getBody()->write('404 pdoException');

            $response = $this->responseFactory->createResponse(500);
            $response->getBody()->write((string)json_encode($result));
            return $response;
        }


    }
}

@juanma-mol
Copy link

Ok, I solve the problem:
in src/container.php add:

use Psr\Http\Message\ResponseFactoryInterface;

//..

    ResponseFactoryInterface::class => function (ContainerInterface $container) {
        return $container->get(App::class)->getResponseFactory();
    },

@blacktornado
Copy link

blacktornado commented Oct 5, 2020

Hi @odan ! fantastic tutorial . I am following it right now, but i customized it as per my requirement and didn't worked so far. If i start my local server with php -S 0.0.0.0:8080 -t public public/index.php it works like a charm. but on real server i cannot start the server explicitly for this project as there are other php project running. so i had to add $app->setBasePath('/somefolder/slim4')

here in container.php if i add these lines it wont work like this

`App::class => function (ContainerInterface $container) {  
     AppFactory::setContainer($container);
     $app = AppFactory::create();
     $app->setBasePath('/somefolder/slim4'); //this doesn't work from here rather had to write again in router.php
     //var_dump($app);
    #return AppFactory::create();
    return $app;
},`

i am again adding / creating the instance here in routes.php // also notice i have removed return function(App $app){ //this is not working

$app = AppFactory::create();
$app->setBasePath('/somefolder/slim4'); //now it works

$app->get('/', \App\Action\HomeAction::class)->setName('home');

and in bootstrap.php i changed it to

// Register routes (require __DIR__ . '/routes.php');//($app); //notice i removed $app from passing as we have removed return function(App $app){ from router.php

but i can see its a bad way to fix this issue, any other way or am i missing something. how to fix this
Thanks in advance

@odan
Copy link
Author

odan commented Oct 5, 2020

Hi @blacktornado

The Slim 4 tutorial already contains the solution for exact this issue.
Just install and add the BasePathMiddleware as described here:

https://odan.github.io/2019/11/05/slim4-tutorial.html#base-path

@blacktornado
Copy link

blacktornado commented Oct 6, 2020

Hi @odan, thank you for the response.
just downloaded your code from github, yes BasePathMiddleware added. As said everything works fine when i start the php server inside public/index of my slim4 project directory like `php -S 0.0.0.0:8080 -t public public/index.php

but i get 404 when i start the server outside the slim4 folder like php -S 0.0.0.0:8080
i am wondering how the program is determining which folder to run in browser because there are so many other php project.

error image

@odan
Copy link
Author

odan commented Oct 6, 2020

@blacktornado You screenshot shows that you start the php internal webserver directly in your Desktop directory. I'm sure this is not the correct path where you have stored the public/ directory. Change into the project directory and then into the public/ directory.
I think in your specific case it should be:

cd c:\Users\black\Desktop\slim
php -S localhost:8000 -t public

You can also try this:

cd c:\Users\black\Desktop\slim\public
php -S localhost:8000

Then navigate to: http://localhost:8000/

For technical questions create an issue here: https://github.com/odan/slim4-tutorial/issues

Read more: odan/slim4-skeleton#7

@blacktornado
Copy link

blacktornado commented Oct 6, 2020

yes @odan i understand your point as it is looking for the public directory which has index.php, as i am using php internal web server.

coming to another scenario where i have web server that points to htdocs and my slim is inside htdocs/somefolder/slim i am hitting the url with 10.xxx.xxx.x:port/somefolder/slim the request lands successfully to public/index.php from there it goes to bootstrap.php to container.php and finally lands to routes.php where if i do a var_dump($app) it gives me resultset but i am getting 404 error with

Type: Slim\Exception\HttpNotFoundException
code: 404
Message: Not Found
File: /www/zendphp7/htdocs/somefolder/slim/vendor/slim/slim/Middleware/RoutingMiddleware.php

to make it work if i remove this line return function(App $app){ from router.php and again add this two lines in router.php
$app = AppFactory::create();
$app->setBasePath('/somefolder/slim');
its working.

@odan
Copy link
Author

odan commented Oct 6, 2020

@blacktornado I think it makes no sense to create a new $app instance, because per request there should be only one Slim instance.
If you have further technical questions, please create an issue here: https://github.com/odan/slim4-tutorial/issues

@marcusball
Copy link

marcusball commented May 29, 2021

This is a great tutorial, thank you so much!

Is there any particular reason to separate the Read and Create into separate repositories and services? In this example, would it be against design patterns to use a single UserRepository containing both insertUser and getUserById methods? Similarly, is it suggested to not combine the Service methods into a single UserService?

@odan
Copy link
Author

odan commented May 29, 2021

Hi @marcusball Please note that this is not an Active-Record (anti-) pattern. The shown pattern follows Command–query separation (CQS) on routing level. So read and write access is separated. To respect the SOLID (see SRP) principles, the service class is responsible only for one specific task (use case) and not more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment