Laravel and Codeception - test fails on PUT forms - php

I have a resource controller in laravel to manage my users. This creates a route to update users info that takes a request with HTTP PUT method.
This shows artisan route:list command output:
+--------+--------------------------------+-----------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------+------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+--------------------------------+-----------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------+------------+
...
| | PUT | users/{users} | users.update | App\Http\Controllers\Users\UsersController#update | auth |
It works correctly on my web browser but when I try to run a test with codeception and I submit the form I get a method not allowed exception and the test fails.
I tried to see why this is happening and it seems to be the request made by codeception. That request is made with POST instead of PUT method preventing Laravel from matching the route.
HTML forms doesn't suport PUT methods so Laravel Form helper class creates the form as follows:
<form method="POST" action="https://myapp.dev/users/172" accept-charset="UTF-8">
<input name="_method" value="PUT" type="hidden">
...
However, it seems that codeception is not reading the _method value.
How can I fix this?
EDIT: Looking deeply on the code I found that test don't override the request method beacause of a constant in th Request class called Request::$httpMethodParameterOverride.
/**
* Gets the request "intended" method.
*
* If the X-HTTP-Method-Override header is set, and if the method is a POST,
* then it is used to determine the "real" intended HTTP method.
*
* The _method request parameter can also be used to determine the HTTP method,
* but only if enableHttpMethodParameterOverride() has been called.
*
* The method is always an uppercased string.
*
* #return string The request method
*
* #api
*
* #see getRealMethod()
*/
public function getMethod()
{
if (null === $this->method) {
$this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET'));
if ('POST' === $this->method) {
if ($method = $this->headers->get('X-HTTP-METHOD-OVERRIDE')) {
$this->method = strtoupper($method);
} elseif (self::$httpMethodParameterOverride) {
$this->method = strtoupper($this->request->get('_method', $this->query->get('_method', 'POST')));
}
}
}
return $this->method;
}
The previous constant value should be true but shomehow, when I run the test its value is false.

I found a solution but I don't think this is the right place to write it.
I added a simple line of code on the Connector\Laravel5 class.
public function __construct($module)
{
$this->module = $module;
$this->initialize();
$components = parse_url($this->app['config']->get('app.url', 'http://localhost'));
$host = isset($components['host']) ? $components['host'] : 'localhost';
parent::__construct($this->app, ['HTTP_HOST' => $host]);
// Parent constructor defaults to not following redirects
$this->followRedirects(true);
// Added to solve the problem of overriding the request method
Request::enableHttpMethodParameterOverride();
}
This solves my problem.

You can not use PUT method in HTML form tag. For that you need to use laravel's blade template format to define form tag.
e.g.
{!! Form::open(['url' => 'users/{users}','method' => 'put','id' => 'form' ]) !!}
Also you can use route attribute to define route instead of url.

Related

Symfony 4 Route gets mismatched - redirectToRoute leads to the current page

Dear intelligent hive,
I got problems with redirect in symfony 4.
Of course I tried to find a similar question, but found no solution.
I just started learning Symfony 4 by working through some tutorials. (AND I LOVE IT!!!)
The program is a really simple blog example on a local apache server (maybe this is important).
Means List, Add, Show
So my problem is in the add method.
After adding an article (which works fine), I want to use redirectToRout('article_list').
But that doesn't lead me anywhere except on the add-page.
When I debug my routes, I can see the correct routes:
article_list ANY ANY ANY /article
article_delete DELETE ANY ANY /article/delete/{id}
article_add ANY ANY ANY /article/add
article_show ANY ANY ANY /article/{id}
And these routes all work fine, when I type them in manually.
The add-function is also pretty simple:
/**
* #Route(
* "/article/add",
* name="article_add",
* )
*/
public function add(Request $request)
{
$article = new Article();
$form = $this->createFormBuilder($article)
[...]
->getForm();
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()){
$article = $form->getData();
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($article);
$entityManager->flush();
$this->redirectToRoute('article_list');
}
return $this->render('article/new.html.twig', array(
'form' => $form->createView()
));
}
As well as the list-function
/**
* #Route(
* "/article",
* name="article_list",
* )
*/
public function index()
{
$articles = $this->getDoctrine()->getRepository(Article::class)->findAll();
return $this->render('article/list.html.twig', [
'articles' => $articles,
]);
}
As you can see, the name in the annotations is correct.
When I change the redirectToRoute-Method to
redirectToRoute("/article");
I get the error "Unable to generate a URL for the named route "/article" as such route does not exist.
In the log, I see Matched route "article_add"
So the route "/article" was matched to the route "article_add". Thats not, what I would expect....
So I kind of have the feeling, that something on the add-page is ignoring my routes.
Does anybody have any idea, what simple problem I'm not able to see?
Thanks!
I would really like to understand, whats going wrong here...
You're not returning the redirection. Do this:
return $this->redirectToRoute('article_list');
That method only creates the response and you need to explicitly return.

Laravel route redirecting with data

I have a basic route that looks like this:
Route::prefix('/group')->group(function () {
// Some routes here
Route::prefix('/{uuid}')->group(function () {
// Some routes here
Route::get('/user/{id}', 'Controller#preview')->name('view-user')->where('id', '[0-9]+');
}
}
The logic is that I want the id to be only numerical value. What I want to do now is, to declare a redirection to this, if the value is non-numerical. Let's say the input of id is fs. In that case I would want it to redirect to id with value 1.
I tried using Route:redirect, but could not make it work. It looks something like this:
Route::redirect('/group/{uuid}/user/{id}', '/group/{uuid}/user/1')->where('id', '[^0-9]+');
I would prefer to put the redirect inside the groups, but it can be outside if this is the only way. Any help will be greatly appreciated.
What happens is, that I get a 404 error if I have the route redirect declared.
EDIT: I want to do it in the routes/web.php file. I know how to do it in the controller, but it is not what I need in the current case.
Closures are not an option either, because that would prevent routes caching.
Following up on the comment
You can create a Route in routes/web.php file that catches non-digit ids and redirect this to 'view-user' with id=1
It would look something like this
Route::get('/group/{uuid}/user/{id}', function ($uuid, $id) {
return redirect('view-user', ['uuid' => $uuid, 'id' => 1]);
})->where('id', '[^0-9]+');
// and then below have your normal route
Route::get('/group/{uuid}/user/{id}', 'Controller#preview')->name('view-user')->where('id', '[0-9]+');
Update
Following you comment that you do not want to use closures.
Change the "bad input route" to
Route::get('/group/{uuid}/user/{id}', 'Controller#redirectBadInput')->where('id', '[^0-9]+');
and then add the method in class Controller:
public function redirectBadInput ($uuid, $id) {
return redirect('view-user', ['uuid' => $uuid, 'id' => 1]);
}
You can see more in this SO thread about redirects and caching.
You declared it inverted.
In Laravel you can redirect passing parameters in this way:
You can pass the name instead of the url and simply pass variables.
Redirect::route('view-user', [$uuid, $id])
I think that you can do it inside of the controller of the router, with a logic like this:
class Controller {
// previous code ..
public function preview($uuid, $id) {
if(! is_numeric($id))
return redirect("/my-url/1");
// run the code below if $id is a numeric value..
// if not, return to some url with the id = 1
}
}
I think that there is no way to override the 'where' function of laravel, but I guess that have something like that in Routing Bindings:
Alternatively, you may override the resolveRouteBinding method on your Eloquent model. This method will receive the value of the URI segment and should return the instance of the class that should be injected into the route:
/**
* Retrieve the model for a bound value.
*
* #param mixed $value
* #return \Illuminate\Database\Eloquent\Model|null
*/
public function resolveRouteBinding($value)
{
return $this->where('name', $value)->first() ?? abort(404);
}
But it's require that you manage consise model's values instead of ids of whatever you want.
assign route name in route as like.
return Redirect::route('view-user', ['uuid'=>$uuid,'id'=>$id]);
As you want in web.php file then.
Route::get('/group/{uuid}/user/{id}', function($uuid, $id){
echo $uuid;
echo $id;
})->name('view-user')->where('id', '[0-9]+');

Laravel - How to pass variable from middleware to controller

I'm trying to pass a token from middleware to view and controller. But all the steps I've tried:
Laravel - Passing variables from Middleware to controller/route
Pass variable from middleware to templates
Pass variable from middleware to view via controller in Laravel 5.2
Haven't helped much. Here is my setup:
Requests come in the form of:
https://website.com/reviews?linker=129b08e19014420049da7d6d7aa8fc35fc6279c4
Then gets parsed and checked by middleware:
Middleware
class CheckReviewLink
{
/**
* Check Review Link - granting clients access to submit review
* =================
* Check that user's link matches the 40 character string generated for user
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
$url = $_SERVER['REQUEST_URI'];
$parts = parse_url($url);
$data['token'] = parse_str($parts['query'], $query);
$testimonies = Testimony::all();
foreach ($testimonies as $testimony) {
if ($query['linker'] == $testimony->token) {
Session::flash('token', $data);
return $next($request);
}
}
}
}
** View **
<div class="col-lg-6">
<article>
<input disabled type="text" placeholder="{{Session::get('token', $data)}}" id="token" name="token" size="100" class="form-control border-form white font4light">
</article>
</div>
When I go to get the session data within my view/controller, I get the error:
Undefined variable: data
Any ideas anyone?
Session flash storage is for retaining data between execution times of the application, ie passing from one page load to the other. In your case you are only wanting to pass data from one part of the app to the other during a single run, thus vars in the memory will be cleaner and quicker.
This guy answered it already: Laravel Middleware return variable to controller
You now have better and clearer control of the code and can pass through to, if required, your data to the views and don't have to worry about clearing up your old session data.
First of all you don't need first line in your handle method. You aren't using $response variable.
Secondly use $request->url() instead of $_SERVER['REQUEST_URI'].
And your answer is to simply use session('token') to gat token you want.
If you want to retrieve a session data, you just have to call the session() helper method, and give it the key of the data that you want:
$myToken = session('token');
But, in your case, your are storing an associative array to the session, so when you call it, you have to do like this:
$myToken = session('token')['token'];
So in your view, you will end up with:
<div class="col-lg-6">
<article>
<input disabled type="text" placeholder="{{ session('token')['token'] }}" id="token" name="token" size="100" class="form-control border-form white font4light">
</article>
</div>
You don't need to use a variable when you're trying to get data from the session. So, do this instead:
{{ Session::get('token') }}
Or simply:
{{ session('token') }}
Also, instead of flash() method do this:
session(['token' => $data]);
And then delete it manually if needed:
session()->forget('token');

TYPO3: Keep forms filled after form submit

I created an extension which allows the user to sign up via the frontend. I couldn't use working ones because the client requested special tasks.
This is the code which detects taken usernames.
public function createAction(\Vendor\Feregister\Domain\Model\FeUserX $newFeUserX)
{
$uname = $newFeUserX->getUsername();
$select_query = '*';
$from_table = 'fe_users';
$where_clause = 'username="'.$uname.'"';
$test = $GLOBALS['TYPO3_DB']->exec_SELECTquery($select_query, $from_table, $where_clause);
if ($GLOBALS['TYPO3_DB']->sql_num_rows($test)) {
$this->addFlashMessage('Username is already taken.', '', \TYPO3\CMS\Core\Messaging\AbstractMessage::ERROR);
$this->redirect('new');
} else {
// do stuff when the username isn't taken yet
}
}
But unfortunately and obivously, when redirecting back to the new action, the fields are empty again.
Is there a way to pass the arguments back to the new action and fill the forms?
Yes, and extbase has a standardized way to do this. It works as follows:
If an action is called, its parameters are validated, except if validation is switched off in the doc comments. If validation fails, the previous action (the one whose view contained the submitted form) is called again, with the same parameters.
You can use this as follows:
/**
* #param \Vendor\Feregister\Domain\Model\FeUserX $newFeUserX
* #ignorevalidation $newFeUserX
*/
public function newAction(\Vendor\Feregister\Domain\Model\FeUserX $newFeUserX = null)
{
$this->view->assign('user', $newFeUserX);
// View renders form with name="newFeUserX" and object="{user}",
// action="create", fields use the property-attribute to fill
// in values and field names.
}
/**
* #param \Vendor\Feregister\Domain\Model\FeUserX $newFeUserX
* #validate $newFeUserX \Vendor\Feregister\Validator\UsernameDoesNotExistValidator
*/
public function createAction(\Vendor\Feregister\Domain\Model\FeUserX $newFeUserX)
{
// Do something with the user - you can be sure the username
// is not yet taken
}
The class \Vendor\Feregister\Validator\UsernameDoesNotExistValidator is a custom validator that implements the ValidatorInterface, or extends AbstractValidator. It should basically do the validation you are doing in your createAction (maybe using a repository instead of $GLOBALS['TYPO3_DB']). A validator returns errors in a standard way, making it easier to show nice error messages and localize them.
If the validation fails, extbase will try to forward to the action that rendered the form, in this case the new-action. In this case, it will work, because of the #ignorevalidation annotation on the new-action.
In addition, information about validation errors are available in the view, you can render them using the ViewHelper f:form.validationResults.

Optional get parameter (with slashes) in default / route

The main page on my website is an empty link, like:
www.randomlink.com/
That's the controller with the "/" route. The problem is that I have to use get parameters here, according to the following pattern:
key1/value1/key2/value2
I add these parameters on form submit, and the form redirects back to the main page.
The problem is that, as you can see, I get:
www.randomlink.com/key1/value1/key2/value2
And thus it opens key1 controller, instead of the default one.
/**
* Display dashboard
*
* #Route("/{path}",
* name="dashboard",
* defaults={"path" = "-1"},
* requirements={"path" = ".+"})
* #Template()
*/
public function displayAction($path, Request $request)
{
if($_POST)
{
// add get parameters to $path
return $this->redirect($this->generateUrl('dashboard', ['path' => $path]));
}
// do something
}
How can I solve this issue?
Probably your routing configuration order is not correct: see "Earlier Routes always Win" in the docs
Workaround: What about using query string like: www.randomlink.com/?path=key1/value1/key2/value2, then $request->query->get('path') ?

Categories