I am trying to setup a project for an API using slim framework version 3, I don't know who made the PSR-7 and marked the response object as immutable, I don't see any use in that (IMHO. please explain me if I am wrong). Things were much easier when it was slim 2. Now I came back to slim after a long time.
I have a route which is a post method, I am getting data and saving it to the database and I am trying to send 201 as the response code. all the examples and the documentation is showing you how can you change the response code within the index.php file itself, But I am trying to change it from a response builder which I have tried to use the factory pattern to provide different responses. The problem is the response code always stays 200 no matter what function I call from the response builder class. I tried many forums and different ways of slim but still couldn't able to pull this up. I almost decided to give up on a PSR 7 router implementation and going to implement my own routing solution. But I remember not to reinvent the wheel again so I came here as a final try. Below is the code.
the route definition
$app->post('/users', function(ServerRequestInterface $req, ResponseInterface $res) {
$data = $req->getParsedBody();
$model = new \Apex\Models\User(ApexDB::getInstance());
$jsonBuilder = ApexResponse::getBuilder('JSON', $res);
$control = new \Apex\Controllers\User($model, $jsonBuilder);
$control->create($data);
});
the controller method (abstract I am just setting it up)
public function create($data) {
if($this->model->save($data)) {
$this->response->build($data,201);
} else {
$this->response->build('error',400);
}
}
the JSON builder
class JSONBuilder implements Response
{
public $response;
public function __construct($response)
{
$this->response = $response;
}
public function build($data, $status)
{
$response = $this->response->withJSON($data,$status);
return $response;
}
}
can anyone point me in the right direction?
The PSR-7 decision to use immutable objects for Request and Response is documented in the Why value objects? section of the Meta document.
With Slim 3, you must always return a Response instance from the controller method.
$app->post('/users', function(ServerRequestInterface $req, ResponseInterface $res) {
$data = $req->getParsedBody();
$model = new \Apex\Models\User(ApexDB::getInstance());
$jsonBuilder = ApexResponse::getBuilder('JSON', $res);
$control = new \Apex\Controllers\User($model, $jsonBuilder);
return $control->create($data);
});
and then your create method also needs to return the $response:
public function create($data) {
if($this->model->save($data)) {
$this->response->build($data,201);
} else {
$this->response->build('error',400);
}
return $this->response;
}
It should then work.
However, you can use the controller method directly from the route declaration and avoid the need for a the closure:
$app->post('/users', `Apex\Controllers\User::create`);
The controller's create method would then look like this:
namespace Apex\Controllers;
class User
{
public function create($request, $response)
{
$data = $request->getParsedBody();
$model = new \Apex\Models\User(ApexDB::getInstance());
$jsonBuilder = ApexResponse::getBuilder('JSON', $response);
if ($model->save($data)) {
$response = $jsonBuilder->build($data, 201);
} else {
$response = $jsonBuilder->build('error', 400);
}
return $response;
}
}
Finally, consider rka-content-type-renderer instead of JsonBuilder, though maybe it does more than you've shown.
Update:
Ideally you'd use constructor injection to inject the User model into the controller. To do this:
Update your controller:
namespace Apex\Controllers;
use Apex\Models\User as UserModel;
class User
{
protected $userModel;
public function __construct(UserModel $userModel)
{
$this->userModel = $userModel;
}
public function create($request, $response)
{
$data = $request->getParsedBody();
$jsonBuilder = ApexResponse::getBuilder('JSON', $response);
if ($this->userModel->save($data)) {
$response = $jsonBuilder->build($data, 201);
} else {
$response = $jsonBuilder->build('error', 400);
}
return $response;
}
}
Write a factory for the Pimple dependency injection container:
$container = $app->getContainer();
$container['Apex\Controllers\User'] = function ($c) {
$userModel = new \Apex\Models\User(ApexDB::getInstance());
return new \ApexController\User($userModel);
};
Related
I am trying to apply a principle I read on Uncle Bob's book "Clean Code", which states that the optimal amount of parameters for a method is zero. I found this is very hard to accomplish, but anyways here I am trying it and I'd like your help to understand the best way to come closer to it in my code.
So far, I have a controller and a simple service to do the job I want. My controller is implemented as follows:
<?php
namespace App\Http\Controllers\Cms;
use App\Http\Controllers\Controller;
use App\Http\Requests\GroupRequest;
use App\Models\Group;
use App\Services\GroupsService;
use Illuminate\Http\Request;
class GroupsController extends Controller
{
protected $group_service;
public function __construct(GroupsService $group_service)
{
$this->group_service = $group_service;
}
public function index()
{
$groups = $this->group_service->listAll();
return view('cms.groups.index', compact('groups'));
}
public function store(GroupRequest $request)
{
$result = $this->group_service->createGroupWith($request->all());
return redirect()->back()->with('message', $result['msg']);
}
public function update(GroupRequest $request, Group $group)
{
$result = $this->group_service->updateAGroupWith($request->all(), $group);
return redirect()->back()->with('message', $result['msg']);
}
}
And bellow is my service
<?php
namespace App\Services;
use App\Models\Group;
use Illuminate\Support\Facades\DB;
class GroupsService
{
public function listAll()
{
$groups = Group::all();
return $groups;
}
public function createGroupWith($data)
{
try {
DB::beginTransaction();
$modules_id = array_pop($data);
$group = Group::create($data);
$group->modules()->attach($modules_id);
DB::commit();
return ['msg' => 'Grupo criado com sucesso!'];
} catch (\Throwable $error) {
DB::rollBack();
return ['msg' => $error->getMessage()];
}
}
public function updateAGroupWith($data, $group)
{
try {
DB::beginTransaction();
$modules_ids = array_pop($data);
$group->update($data);
$group->modules()->sync($modules_ids);
DB::commit();
return ['msg' => 'O grupo foi atualizado com sucesso!'];
} catch (\Throwable $error) {
DB::rollBack();
return ['msg' => $error->getMessage()];
}
}
}
As you can see, my service is instanciated by Laravel on the controller's __construct method and is used on the rest of my methods. The problem with this is that when storing or updating a register, I need to pass the data and (for the update) the gruop I want to update. This violates Uncle Bob's ideal of zero parameters and thus my question: is there a way to solve this?
I have come up with some ideas. The first one would be to continue instanciating the service as it is already done, but having properties such as group and data defined before calling the method I want. Something as implemented bellow. In this case the problem is that if I want to be really idealistic, I don't if I can keep accessing properties from an object and changing them, I believe I'd need to instanciate the object with it's state already defined anyways.
public function update(GroupRequest $request, Group $group)
{
$this->group_service->group = $group;
$this->group_service->data = $request->all();
$result = $this->group_service->updateAGroup();
return redirect()->back()->with('message', $result['msg']);
}
The second ideia would be to instanciate the service as I need it on each method:
public function update(GroupRequest $request, Group $group)
{
$service = new GroupService($data, $group);
$result = $service->updateAGroup();
return redirect()->back()->with('message', $result['msg']);
}
The only problem with the idea above is that in the fist method, the one for storing, I'd need to make the $group parameter for the $service = new GroupService($data, $group); statement optional. But I don't know if that's really a problem or a whim of mine.
And there's a third idea, but it seems way too much. It would be to make have a service per case (storing, updating and deleting maybe) and then I wouldn't have to worry with having optional parameters on the __construct method.
Anyways, I'd like to know your ideas, comments, and critiques too. Please take into consideration that I know this is very idealistic, but I'd really like to know how to come closer to it using with the patterns already defined on Laravel.
Thanks in advance.
I'm having problem sending on the second request. I tried to use Curl but the problem still persist.
I have a class that use guzzle to call to an existing api it look like this.
class HttpCall
{
public static function request($method, $endpoint, $payload)
{
$client = new Client();
$response = $client->request(...);
return $response;
}
}
Then I have a service class that use the HttpCall to fetch some data on another server
The flow is like this
Search Name (Request to another endpoint) > Update Data (Request to another endpoint)
so this two flow search name and update data each request a token to the server
Authenticate (Request to login and get token) > Search Name
Authenticate (Request to login and get token) > Update data
My service class is like this
class MyService
{
public function searchName($name)
{
$request = HttpCall::request(...);
return $request;
}
public function updateData($payload)
{
$request = HttpCall::request(...);
return $request;
}
}
then in my Class that actually interact with the events
class MyClass
{
public function __construct(MyService $service)
{
$this->service = $service;
}
public function update()
{
// When I remove this, It's working and hardcoded some data
$data = $this->service->searchName('test');
$updateData = [...];
$this->service->updateData($updateData);
}
}
I'm not quite sure what's happening why the request seem's to fail (sometimes)
Thanks for help guys
I've been testing the new Slim 4 framework and redirects work fine for me in normal classes, but I cannot seem to get them working in middleware, where a response is dynamically generated (apparently?) by the Request Handler. When I try to redirect with a Location header, it simply fails to redirect, and my route continues to the original location.
Here’s a basic version of my authentication middleware for testing:
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
class AuthMiddleware extends Middleware {
public function __invoke(Request $request, RequestHandler $handler): Response {
$response = $handler->handle($request);
$loggedInTest = false;
if ($loggedInTest) {
echo "User authorized.";
return $response;
} else {
echo "User NOT authorized.";
return $response->withHeader('Location', '/users/login')->withStatus(302);
}
}
}
Has anybody got this to work? And if so, how did you accomplish it? Thanks in advance.
I think I see the problem with this code.
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
class AuthMiddleware extends Middleware {
public function __invoke(Request $request, RequestHandler $handler): Response {
$response = $handler->handle($request);
$loggedInTest = false;
if ($loggedInTest) {
echo "User authorized.";
return $response;
} else {
echo "User NOT authorized.";
return $response->withHeader('Location', '/users/login')->withStatus(302);
}
}
}
When you call $handler->handle($request), that processes the request normally and calls whatever closure is supposed to handle the route. The response hasn't been completed yet, you can still append stuff to it, but the headers are already set, so you can't do a redirect, because the headers are done.
Maybe try this:
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
class AuthMiddleware extends Middleware {
public function __invoke(Request $request, RequestHandler $handler): ResponseInterface {
$loggedInTest = false;
if ($loggedInTest) {
$response = $handler->handle($request);
echo "User authorized.";
return $response;
} else {
$response = new Response();
// echo "User NOT authorized.";
return $response->withHeader('Location', '/users/login')->withStatus(302);
}
}
}
If the login test fails, we never call $handler->handle(), so the normal response doesn't get generated. Meanwhile, we create a new response.
Note that the ResponseInterface and Response can't both be called Response in the same file, so I had to remove that alias, and just call the ResponseInterface by its true name. You could give it a different alias, but I think that would only create more confusion.
Also, I commented out the echo before the redirect. I think this echo will force headers to be sent automatically, which will break the redirect. Unless Slim 4 is doing output buffering, in which case you're still not going to see it, because the redirect will immediately send you to a different page. Anyway, I commented it out to give the code the best chance of working but left it in place for reference.
Anyway, I think if you make that little change, everything will work. Of course, this post is almost a year old, so you've probably solved this on your own, switched to F3, or abandoned the project by now. But hopefully, this will be helpful to someone else. That's the whole point of StackOverflow, right?
eimajenthat is right, except that you cannot create an instance of interface.
Try this instead:
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
class AuthMiddleware extends Middleware {
public function __invoke(Request $request, RequestHandler $handler): Response {
global $app; // Assuming $app is your global object
$loggedInTest = false;
if ($loggedInTest) {
$response = $handler->handle($request);
echo "User authorized.";
return $response;
} else {
$response = $app->getResponseFactory()->createResponse();
// echo "User NOT authorized.";
return $response->withHeader('Location', '/users/login')->withStatus(302);
}
}
}
I was growing so frustrated by Slim 4 and redirect issues that I took a look at FatFreeFramework and had the exact same problem. So I knew it was something I was doing. My code was putting the app into a never-ending redirect loop. I can make it work by validating the redirect URL like so in FatFreeFramework:
class Controller {
protected $f3;
public function __construct() {
$isLoggedIn = false;
$this->f3 = Base::instance();
if ($isLoggedIn == false && $_SERVER['REQUEST_URI'] != '/login') {
$this->f3->reroute('/login');
exit();
}
}
}
Therefore, although I haven't actually taken the time to test it, I'm assuming I could fix it in Slim 4 by doing something like:
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
class AuthMiddleware extends Middleware {
public function __invoke(Request $request, RequestHandler $handler): Response {
$response = $handler->handle($request);
$loggedInTest = false;
if (!$loggedInTest && $_SERVER['REQUEST_URI'] != '/user/login') {
return return $response->withHeader('Location', '/users/login')->withStatus(302);
} else {
return $response;
}
}
}
Does anybody have another idea for how to break a continuous redirect loop? Or is the $_SERVER variable the best option?
Thanks in advance.
Use 2 response
namespace App\middleware;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response as Response7;
use Psr\Http\Message\ResponseInterface as Response;
final class OtorisasiAdmin {
public function __invoke(Request $request, RequestHandler $handler): Response {
$session = new \Classes\session();
$session->start();
$isAdmin=($session->has("login","admin"))?true:false;
if(!$isAdmin){
$response = new Response7();
$error = file_get_contents(__dir__."/../../src/error/404.html");
$response->getBody()->write($error);
return $response->withStatus(404);
}
$response=$handler->handle($request);
return $response;
}
}
I'm setting up slim router v4, and I'd like to be be able to dynamically call the controller methods, using the placeholder from the route.
I.e when a request is made to 'example.com/users/{action}', the router would call the method from Users.php controller automatically without me having to specify the routes manually.
Basically I'm trying to avoid manually adding over 100 group->get(...) when they're all under /user route.
namespace core\router;
use Slim\Interfaces\RouteCollectorProxyInterface;
use app\controllers\users;
$app->group('/user', function(RouteCollectorProxyInterface $group){
$group->get('/get-name', '\Users:name')
$group->get('/get-personality', '\Users:personality');
});
Further explanation is provided here but I'm not sure how to go about this.
The way I would suggest doing this is having a single, catch all route with a placeholder. You can then set action to an invocable controller, and execute a method based on the route parameter.
Route:
$app->get('/user/{method}', Users::class);
Controller
class Users
{
public function __invoke(Request $request, Response $response, $args)
{
if (empty($args['method'])) {
throw new InvalidArgumentException();
}
$methodName = toCamelCase($args['method']);
if (!method_exists($this, $methodName)) {
throw new InvalidArgumentException();
}
return $this->{$methodName};
}
public function getName(Request $request, Response $response)
{
// ...
}
public function getPersonality(Request $request, Response $response)
{
// ...
}
}
This is my original code:
$app->post('/api/user', function (Request $request, Response $response, array $args) use ($tokenAuth) {
$tokenAuth->parseHeaders(); // validate token
//...do stuff with $tokenAuth data
$response->getBody()->write(json_encode(...)));
return $response;
});
Notice the use ($tokenAuth) this allows me to use this object within the method.
How can I use this method below in the same manner?
$app->post('/api/user', \UserController::class . ':add');
which works fine when the route hits in the class UserController:
class UserController {
public function add($request, $response, $args) { ...}
}
How can I pass $tokenAuth to this?
You can register $tokenAuth and UserController to dependency manager and pass $tokenAuth to UserController via dependency injection merchanism. For example:
<?php
namespace Your\Own\NameSpace;
use Your\Own\NameSpace\TokenAuth;
class UserController
{
private $tokenAuth;
public function __construct(TokenAuth $tokenAuth)
{
$this->tokenAuth = $tokenAuth;
}
public function add($request, $response, $args)
{
$this->tokenAuth->parseHeaders();
...
}
}
In your dependency registration code (assuming using Pimple):
<?php
use Your\Own\NameSpace\TokenAuth;
use Your\Own\NameSpace\UserController;
$container[TokenAuth::class] = function ($c) {
return new TokenAuth();
};
$container[UserController::class] = function ($c) {
$tokenAuth = $c->get(TokenAuth::class);
return new UserController($tokenAuth);
};
I ended up using middleware to authenticate on the route:
$mw = function($request, $response, $next){
$tokenAuth = new \TokenAuth();
$tokenAuth->parseHeaders();
$response = $next($request, $response);
return $response;
};
$app = new \Slim\App();
$app->post('/api/user', \UserController::class . ':add')->add($mw);
$app->run();
Take a look at your original code, you can use $tokenAuth (and it works), it means that you defined it globally before.
So, in your add method, just declare it as a global variable: global $tokenAuth; then use it;
public function add($request, $response, $args) {
global $tokenAuth;
$tokenAuth->parseHeaders();
...
}