Call to undefined method Slim\\Psr7\\Response::write() - php

I have just installed composer and create a project using composer create-project slim\slim-skeleton MyApi. using Slim V4. Creation was successful but I failed to run routes requiring database operations.
<?php
// ...
require __DIR__ . '/../includes/DbOperations.php';
// ...
$app->post('/hello', function (Request $request, Response $response) {
$response->getBody()->write('Hello world!');
return $response;
});
$app->post('/createuser', function (Request $request, Response $response) {
if (!haveEmptyParameters(array('uname', 'uphone', 'upassword', 'uposition', 'ucreated'), $request, $response)) {
$request_data = $request->getParsedBody();
$uname = $request_data['uname'];
$uphone = $request_data['uphone'];
$upassword = $request_data['upassword'];
$uposition = $request_data['uposition'];
$ucreated = $request_data['ucreated'];
// ...
}
});
localhost:8080/hello works fine, however localhost:8080/createuser fails with an error { "statusCode": 500, "error": { "type": "SERVER_ERROR", "description": "Call to undefined method Slim\\Psr7\\Response::write()" } }
What may the problem be? Thank you in advance.
With Love from Gulu, Uganda, East Africa.

We cannot see the full content of the /createuser route.
I guess you have to invoke $response->getBody() to write content to the response.
Example
$response->getBody()->write("Hello world");
return $response;

Related

Is there a way to make Symfony controller return nothing?

I use Symfony 6.1 and PHP 8.1. I'm wondering if there is a way to make a Symfony controller return nothing. The why is I'm using Botman and unfortunalty it send itself a response to the client... So I need to make my controller to return nothing.
/**
* #param Request $request
*
* #return Response
*/
#[Route('/botman', methods: 'POST')]
public function test(Request $request): Response
{
DriverManager::loadDriver(WebDriver::class);
$adapter = new FilesystemAdapter();
$botman = BotManFactory::create([], new SymfonyCache($adapter), $request);
$botman->hears('hello', static function (BotMan $bot) {
$bot->reply('Yoooooo'); //<- Send a response to the client
});
$botman->fallback(static function (BotMan $bot) {
$bot->reply('try again'); //<- Send a response to the client
});
$botman->listen(); // Launch the maching callback
return new Response(); //Crash because a response have already been sent
}
This is what I get on my browser inspector.
{
"status": 200,
"messages": [
{
"type": "text",
"text": "try again",
"attachment": null,
"additionalParameters": []
}
]
}{
"type": "https:\/\/tools.ietf.org\/html\/rfc2616#section-10",
"title": "An error occurred",
"status": 500,
"detail": "Warning: Cannot modify header information - headers already sent by (output started at C:\\Users\\cimba\\Documents\\botman\\vendor\\symfony\\http-foundation\\Response.php:1265)",
"class": "ErrorException",
"trace": [...]
}
The only solution I have is to exit(); or die(); instead of the return but I it's not clean for production... Maybe there is a way to avoid BotMan to send it's response or to tell Symfony that I want my controller to return nothing (because botman does without symfony).
I have not used BotMan myself so this is a bit of a guess. You might try seeing if there a BotMan bundle because I suspect that if such a bundle exists then it would deal with this sort of thing as well as eliminating the need to use the static factory method.
However returning an empty response (which means no headers or content) might do the trick as well.
# src/Http/EmptyResponse.php
namespace App\Http;
use Symfony\Component\HttpFoundation\Response;
class EmptyResponse extends Response
{
public function send(): static
{
// Skip sending headers and content
// $this->sendHeaders();
// $this->sendContent();
// Keep some standard Symfony magic
if (\function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} elseif (\function_exists('litespeed_finish_request')) {
litespeed_finish_request();
} elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
static::closeOutputBuffers(0, true);
}
return $this;
}
}
Then just return new EmptyResponse(); in your controller.
I tested this in a regular controller action and Symfony did not complain. But again I have no BotMan setup to test against. Be curious to see if it works.

Route Middleware in Slim 4 doesn't stop invoking the callable in the route

I'm strugling with authorization middleware in Slim4. Here's my code:
$app = AppFactory::create();
$app->add(new Authentication());
$app->group('/providers', function(RouteCollectorProxy $group){
$group->get('/', 'Project\Controller\ProviderController:get');
})->add(new SuperuserAuthorization());
Authentication middleware checks the user and works fine.
The method get in ProviderController is
public function get(Request $request, Response $response): Response{
$payload = [];
foreach(Provider::all() as $provider){
$payload[] = [
'id' => $provider->id,
'name' => $provider->name,
];
}
$response->getBody()->write(json_encode($payload));
return $response;
}
The SuperuserAuthorization looks like this
class SuperuserAuthorization{
public function __invoke(Request $request, RequestHandler $handler): Response{
$response = $handler->handle($request);
$authorization = explode(" ", $request->getHeader('Authorization')[0]);
$user = User::getUserByApiKey($authorization[1]);
if(! Role::isSuperuser($user)){
return $response->withStatus(403);//Forbidden
}
return $response;
}
}
The thing is that even though the user is not a superuser, the application continues executing. As a result I get json with all the providers and http code 403 :/
Shouldn't route middleware stop the request from getting into the app and just return 403 right away?
I know that I can create new empty response with status 403, so the data won't come out, but the point is that the request should never get beyond this middleware, am I right or did I just misunderstand something hereā€¦
Any help will be appreciated :)
------------- SOLUTION ----------------
Thanks to #Nima I solved it. The updated version of middleware is:
class SuperuserAuthorization{
public function __invoke(Request $request, RequestHandler $handler): Response{
$authorization = explode(" ", $request->getHeader('Authorization')[0]);
$user = User::getUserByApiKey($authorization[1]);
if(! Role::isSuperuser($user)){
$response = new Response();
return $response->withStatus(403);//Forbidden
}
return $handler->handle($request);
}
}
Shouldn't route middleware stop the request from getting into the app and just return 403 right away?
Slim 4 uses PSR-15 compatible middlewares. There is good example of how to implement an authorization middleware in PSR-15 meta document. You need to avoid calling $handler->handle($request) if you don't want the request to be processed any further.
As you can see in the example, if the request is not authorized, a response different from the return value of $handler->handle($request) is returned. This means your point saying:
I know that I can create new empty response with status 403, so the data won't come out, but the point is that the request should never get beyond this middleware
is somehow correct, but you should prevent the request from going further by returning appropriate response before invoking the handler, or throwing an exception and let the error handler handle it.
Here is a simple middleware that randomly authorizes some of requests and throws an exception for others:
$app->group('/protected', function($group){
$group->get('/', function($request, $response){
$response->getBody()->write('Some protected response...');
return $response;
});
})->add(function($request, $handler){
// randomly authorize/reject requests
if(rand() % 2) {
// Instead of throwing an exception, you can return an appropriate response
throw new \Slim\Exception\HttpForbiddenException($request);
}
$response = $handler->handle($request);
$response->getBody()->write('(this request was authorized by the middleware)');
return $response;
});
To see different responses, please visit /protected/ path a few times (remember the middleware acts randomly)

Replacement for notFoundHandler setting

I'm migrating from Slim/3 to Slim/4. I've found or figured out replacements for all the features I was using that have been removed, except 404 Not Found Handler (part of the now gone App::$settings):
Slim App::$settings have been removed, multiple middleware have been implemented to replace the functionality from each individual settings.
Is there a middleware for notFoundHandler? If there isn't, how can I implement it?
Mine used to look like this:
use Slim\Container;
$config = new Container();
$config['notFoundHandler'] = function (Container $c) {
return function (Request $request, Response $response) use ($c): Response {
$page = new Alvaro\Pages\Error($c);
return $page->notFound404($request, $response);
};
};
According to Slim 4 documentation on error handling
Each Slim Framework application has an error handler that receives all
uncaught PHP exceptions
You can set a custom error handler to handle each type of exceptions thrown. A list of predefined exception classes is available on same page.
Here is a very basic example of how to register a closure as an error handler, to handle only HttpNotFoundException exceptions. You can also put the handler in a class that extends Slim\Handlers\ErrorHandler. Also, I did not actually use your Alvaro\Pages\Error to generate the response, but changing it should be straightforward:
<?php
require '../vendor/autoload.php';
$app = Slim\Factory\AppFactory::create();
// Define Custom Error Handler
$customErrorHandler = function (
Psr\Http\Message\ServerRequestInterface $request,
\Throwable $exception,
bool $displayErrorDetails,
bool $logErrors,
bool $logErrorDetails
) use ($app) {
$response = $app->getResponseFactory()->createResponse();
// seems the followin can be replaced by your custom response
// $page = new Alvaro\Pages\Error($c);
// return $page->notFound404($request, $response);
$response->getBody()->write('not found');
return $response->withStatus(404);
};
// Add Error Middleware
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
// Register the handler to handle only HttpNotFoundException
// Changing the first parameter registers the error handler for other types of exceptions
$errorMiddleware->setErrorHandler(Slim\Exception\HttpNotFoundException::class, $customErrorHandler);
$app->get('/', function ($request, $response) {
$response->getBody()->write('Hello Slim 4');
return $response;
});
$app->run();
Another approach is to create a generic error handler and register it as the default handler, and inside that handler, decide what response should be sent based on type of exception that is thrown. Something like:
$customErrorHandler = function (
Psr\Http\Message\ServerRequestInterface $request,
\Throwable $exception,
bool $displayErrorDetails,
bool $logErrors,
bool $logErrorDetails
) use ($app) {
$response = $app->getResponseFactory()->createResponse();
if ($exception instanceof HttpNotFoundException) {
$message = 'not found';
$code = 404;
} elseif ($exception instanceof HttpMethodNotAllowedException) {
$message = 'not allowed';
$code = 403;
}
// ...other status codes, messages, or generally other responses for other types of exceptions
$response->getBody()->write($message);
return $response->withStatus($code);
};
Then you can set this as the default error handler:
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
$errorMiddleware->setDefaultErrorHandler($customErrorHandler);

Error Using Middleware in SLIM Framework

I have been at this for hours now and can not seem to figure out why it's not working. This my first time using SLIM and my first exposure to middleware. I am trying to follow the tutorial listed on the slim website but just can't get to work.
My bootstrap code:
<?php
require '../vendor/autoload.php';
$app = new Slim\Slim();
$app->get('/test', function() {
echo 'Hello, World';
});
$mw = function ($request, $response, $next) {
$response->getBody()->write('BEFORE');
$response = $next($request, $response);
$response->getBody()->write('AFTER');
return $response;
};
$app->add($mw);
$app->run();
When I run just my slim url without the middleware it runs fine. I get Hello, World as the output when I runt http://mysite/test. But when I add the middleware code as listed on the slim site I get the following error:
Catchable fatal error: Argument 1 passed to Slim\Slim::add() must be an instance of Slim\Middleware, instance of Closure given, called in /Applications/XAMPP/xamppfiles/htdocs/project/api/public/index.php on line 22 and defined in /Applications/XAMPP/xamppfiles/htdocs/academy/api/vendor/slim/slim/Slim/Slim.php on line 1267
Am I missing something? Does the middleware require some other setup? The slim documentation is not very helpful when it comes to this. Any help appreciated.
You seem to have installed Slim 2. You are also mixing Slim 2 and Slim 3 syntax. To install Slim 3 issue the following command.
$ composer install slim/slim
Then use code like following:
<?php
require "vendor/autoload.php";
$app = new \Slim\App;
$mw = function ($request, $response, $next) {
$response->getBody()->write("BEFORE");
$response = $next($request, $response);
$response->getBody()->write("AFTER");
return $response;
};
$app->add($mw);
$app->get("/test", function ($request, $response) {
echo "Hello, World";
});
$app->run();

Backbone.js and REST api with Silex (PHP)

lets say I have a model called John with those params:
{
Language : {
code : 'gr',
title : 'Greek'
},
Name : 'john'
}
So now when I trigger John.save() it POST those to server:
post params http://o7.no/ypvWNp
with those headers:
headers http://o7.no/x5DVw0
The code in Silex is really simple:
<?php
require_once __DIR__.'/silex.phar';
$app = new Silex\Application();
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
// definitions
$app['debug'] = true;
$app->post('/api/user', function (Request $request) {
var_dump($request->get('Name'));
$params = json_decode(file_get_contents('php://input'));
var_dump($params->Name);
});
$app->run();
but first var_dump return null second var_dump of course works since I'm getting the request directly from php://input resource. I'm wondering how I could get the params using Request object from Silex
Thanks
It's pretty easy actually.
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\ParameterBag;
$app->before(function (Request $request) {
if (0 === strpos($request->headers->get('Content-Type'), 'application/json')) {
$data = json_decode($request->getContent(), true);
$request->request = new ParameterBag(is_array($data) ? $data : array());
}
});
And then an example route:
$app->match('/', function (Request $request) {
return $request->get('foo');
});
And testing with curl:
$ curl http://localhost/foobarbazapp/app.php -d '{"foo": "bar"}' -H 'Content-Type: application/json'
bar
$
Alternatively look at the (slightly outdated) RestServiceProvider.
EDIT: I have turned this answer into a cookbook recipe in the silex documentation.
The way I have done it before is the following:
$app->post('/api/todos', function (Request $request) use ($app) {
$data = json_decode($request->getContent());
$todo = $app['paris']->getModel('Todo')->create();
$todo->title = $data->title;
$todo->save();
return new Response(json_encode($todo->as_array()), 200, array('Content-Type' => 'application/json'));
});
In your backbone collection, add the following:
window.TodoList = Backbone.Collection.extend({
model: Todo,
url: "api/todos",
...
});
I have written up a full step-by-step tutorial here http://cambridgesoftware.co.uk/blog/item/59-backbonejs-%20-php-with-silex-microframework-%20-mysql
I've solved it myself by setting an extra $payload property on the Request object
$app->before(function(Request $request) {
if (stristr($request->getContentType(), 'json')) {
$data = json_decode($request->getContent());
$request->payload = $data;
} else {
$request->payload = null;
}
});

Categories