I was wondering if anyone else came across this problem. I'm going through Jeffrey Way's book on testing in Laravel and I'm on the chapter which explains how to test controllers.
When I follow the examples from the book - I get the message:
Failed asserting that Illuminate\Http\Response Object (...) is an
instance of class "Illuminate\Http\RedirectResponse".
My test is as follow:
public function testStoreFails()
{
$input = ['title' => ''];
$this->mock
->shouldReceive('create')
->once()
->with($input);
$this->app->instance('Post', $this->mock);
$this->post('posts', $input);
$this->assertRedirectedToRoute('posts.create');
$this->assertSessionHasErrors(['title']);
}
And the very simple method in the controller (just to test this specific scenario):
public function create()
{
$input = Input::all();
$validator = Validator::make($input, ['title' => 'required']);
if ($validator->fails()) {
return Redirect::route('posts.create')
->withInput()
->withErrors($validator->messages());
}
$this->post->create($input);
return Redirect::route('posts.index')
->with('flash', 'Your post has been created!');
}
From what I can see the AssertionsTrait::assertRedirectedTo checks for instance of Illuminate\Http\RedirectResponse
/**
* Assert whether the client was redirected to a given URI.
*
* #param string $uri
* #param array $with
* #return void
*/
public function assertRedirectedTo($uri, $with = array())
{
$response = $this->client->getResponse();
$this->assertInstanceOf('Illuminate\Http\RedirectResponse', $response);
$this->assertEquals($this->app['url']->to($uri), $response->headers->get('Location'));
$this->assertSessionHasAll($with);
}
/**
* Assert whether the client was redirected to a given route.
*
* #param string $name
* #param array $parameters
* #param array $with
* #return void
*/
public function assertRedirectedToRoute($name, $parameters = array(), $with = array())
{
$this->assertRedirectedTo($this->app['url']->route($name, $parameters), $with);
}
which should work just fine as the Redirect facade resolves to the Illuminate\Routing\Redirector and its route() method calls createRedirect(), which returns the instance of the Illuminate\Http\RedirectResponse - so not quite sure what's causing it.
UPDATE:
Just checked the code again and it looks like the problem is within AssertionsTrait::assertRedirectedTo() method. The call to $this->client->getResponse() returns instance of Illuminate\Http\Response instead of Illuminate\Http\RedirectResponse - hence the $this->assertInstanceOf('Illuminate\Http\RedirectResponse', $response) call fails. But I'm still not sure why - I'm extending the TestCase which is suppose to take care of all environment setup etc. Any idea?
Posting comment as answer:
Since you're working with a resourceful object, Laravel automatically creates some routes for you, namely:
+-----------------------------+---------------+-------------------------+
| URI | Name | Action |
+-----------------------------+---------------+-------------------------+
| GET|HEAD posts | posts.index | PostsController#index |
| GET|HEAD posts/create | posts.create | PostsController#create |
| POST posts | posts.store | PostsController#store |
| GET|HEAD posts/{posts} | posts.show | PostsController#show |
| GET|HEAD posts/{posts}/edit | posts.edit | PostsController#edit |
| PUT posts/{posts} | posts.update | PostsController#update |
| PATCH posts/{posts} | | PostsController#update |
| DELETE posts/{posts} | posts.destroy | PostsController#destroy |
+-----------------------------+---------------+-------------------------+
As you can see, a POST request to /posts (as you do it in your test) triggers the store() method on your PostsController, not the create() method which you assumed to be under test.
create and store as well as edit and update can be confusing sometimes. Here's the difference:
create() - show the form for creating a new resource
store() - actually create the resource from the posted data
edit($id) - show the form for editing the specified resource
update($id) - actually update the resource with the posted data
Related
I have a controller App/Controllers/Api/Orders.php
<?php
namespace App\Controllers\Api;
use CodeIgniter\RESTful\ResourceController;
class Orders extends ResourceController
{
protected $modelName = 'App\Models\Order';
protected $format = 'json';
public function getIndex()
{
return $this->respond($this->model->findAll());
}
public function delete($id = null)
{
return $this->respond(['test' => 123]);
}
}
When I'm trying to access /api/orders/ with GET request it works fine (so auto routing is working).
But when I'm trying to send a DELETE request: /api/orders/15 I get error 404. What am I doing wrong? The main source of confusion for me is that Codeigniter 4 RESTful documentation seems to talk about manual routes or legacy auto routing but not the improved version where you need to specify methodFunction (like getIndex).
Running php spark routes gives me the following:
+--------------+------------------+------+---------------------------------------+----------------+---------------+
| Method | Route | Name | Handler | Before Filters | After Filters |
+--------------+------------------+------+---------------------------------------+----------------+---------------+
| GET(auto) | api/orders | | \App\Controllers\Api\Orders::getIndex | | toolbar |
| DELETE(auto) | api/orders/[/..] | | \App\Controllers\Api\Orders::delete | <unknown> | <unknown> |
CodeIgniter version: 4.3.1
app/Config/Routes.php
$routes->setDefaultNamespace('App\Controllers');
$routes->setDefaultController('Auth');
$routes->setDefaultMethod('index');
$routes->setTranslateURIDashes(false);
$routes->set404Override();
$routes->get('/', 'Home::index');
app/Config/Filters.php
public array $globals = [
'before' => [
// 'honeypot',
// 'csrf',
// 'invalidchars',
],
'after' => [
'toolbar',
// 'honeypot',
// 'secureheaders',
],
];
Solution:
Beyond prefixing the Controller method with an HTTP verb i.e delete, you need to provide a 'Controller method name' as the suffix. I.e: httpVerbMethodName.
Instead of: ❌
public function delete($id = null)
{
// ...
}
Use this: ✅
public function deleteOrder($id = null)
{
// ...
}
Route endpoint:
DELETE: http://localhost:4599/api/orders/order/15
| DELETE(auto) | api/orders/order[/..] | | \App\Controllers\Api\Orders::deleteOrder | | toolbar
Reference:
URI Segments
The segments in the URL, in following with the Model-View-Controller approach, usually represent:
example.com/class/method/ID
The first segment represents the controller class that should be invoked.
The second segment represents the class method that should be called.
The third, and any additional segments, represent the ID and any variables that will be passed to the controller.
Consider this URI:
example.com/index.php/helloworld/hello/1
In the above example, when you send a HTTP request with GET method, Auto Routing would attempt to find a controller named App\Controllers\Helloworld and executes getHello() method with passing '1' as the first argument.
Note
A controller method that will be executed by Auto Routing (Improved) needs HTTP verb (get, post, put, etc.) prefix like getIndex(), postCreate().
See Auto Routing in Controllers for more info.
hello i got issue with access to few routes when they under restricted from middlewere
my route look like this
Route::group([
'middleware' => ['api','auth:sanctum'],
], function ($router) {
Route::prefix('users')->group(function ($usersRoute) {
Route::post('login', 'Api\Users\UserController#login');
Route::post('logout', 'Api\Users\UserController#logout');
Route::post('refresh', 'Api\Users\UserController#refresh');
Route::post('profile', 'Api\Users\UserController#profile');
});
Route::prefix('leads')->group(function ($leadRoute) {
// Route::get('look' , 'Api\Leads\LeadController#index');
// Route::get('look/{id}' , 'Api\Leads\LeadController#find');
// Route::post('look' , 'Api\Leads\LeadController#create');
// Route::put('look/{id}' , 'Api\Leads\LeadController#update');
// Route::delete('look/{id}' , 'Api\Leads\LeadController#archived');
// Route::put('look/{id}/view' , 'Api\Leads\LeadController#toggleView');
Route::prefix('assets')->group(function ($leadRoute) {
Route::get('/' , 'Api\Leads\AssetController#index');
Route::post('/' , 'Api\Leads\AssetController#create');
Route::get('/show/random' , 'Api\Leads\AssetController#showRandomAsset');
Route::delete('/{id}' , 'Api\Leads\AssetController#archived');
Route::delete('/autoclear' , 'Api\Leads\AssetController#autoClear');
Route::put('/{id}/show' , 'Api\Leads\AssetController#toggleShow');
});
});
});
now i have two controllers that the contractor look like this
class UserController extends BaseAPIController
{
/**
* Create a new AuthController instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('auth:sanctum', ['except' => ['login']]);
}
when i check the route list
api,auth:sanctum |
| | POST | api/users/login | | App\Http\Controllers\Api\Users\UserController#login | api,auth:sanctum |
| | POST | api/users/logout | | App\Http\Controllers\Api\Users\UserController#logout | api,auth:sanctum |
| | POST | api/users/profile | | App\Http\Controllers\Api\Users\UserController#profile | api,auth:sanctum |
| | POST | api/users/refresh | | App\Http\Controllers\Api\Users\UserController#refresh | api,auth:sanctum
what i try is free login route free for all but i get
{
"message": "Unauthenticated."
}
i kinda lost please help
when I check the doc of laravel, I found that:
$this->middleware('subscribed')->except('store');
the second param for function middleware is using for options, so how about try:
$this->middleware('auth:sanctum')->except('login');
From your code, you're including the middleware twice and that's not right.
You should remove the group middleware in the routes file since you're already injecting the auth middleware in the constructor method.
Remove
Route::group([
'middleware' => ['api','auth:sanctum'],
], function ($router) {
After this your code should work as intended because auth:sanctum middleware would be called only once in your controller every time it is instantiated and will exclude the login route.
I'm working on a Laravel project, and I made a simple CRUD system, but I have a small problem
to generate the URL system in my project, I made a Route::macro
and add it to AppServiceProvider:
Route::macro('crud', function () {
Route::group([
], function () {
// Route::resource('', 'CrudController');
Route::get('{model}', 'CrudController#index');
Route::get('{model}/create', 'CrudController#create');
Route::post('{model}', 'CrudController#store'); /** << post **/
Route::get('{model}/{id}', 'CrudController#show');
Route::get('{model}/{id}/edit', 'CrudController#edit');
Route::match(['PUT', 'PATCH'],'{model}/{id}', 'CrudController#update'); /** << post **/
Route::delete('{model}/{id}', 'CrudController#destroy'); /** << post **/
});
});
this works great so far, but the issue is I need to use ->name() with it, and adding the $model parameter to it!
example of what I'm trying to do:
Route::get('{model}', 'CrudController#index')->name('{model}.index');
is it possible?, Thanks in advance
You can get all models names and loop through them and add the model name to the route name prefix at runtime
loop (model in models)
Route::get("{model}","Atcion")->name("{model}.index")
endloop
I hope my answare helps you in your project
Here in this example you can loop over of some numbers and dynamically create some routes:
for ($i = 0; $i < 5; $i++) {
Route::get('test/' . $i, 'Controller#test_' . $i)->name('test.' . $i);
}
You can check that all added routes with "php artisan route:list".
I don't recommend you to do that, but for your case you can somewhere in routes.php define array like this, and loop over on that:
$models = ['user', 'owner', 'admin'];
foreach ($models as $model) {
Route::get($model, 'CrudController#index')->name($model . '.index');
}
Or you can define that array in configs (for example in "config/app.php") like:
'models' => ['user', 'owner', 'admin'];
And in routes.php you can just retrieve that with this (don't forget to run "php artisan config:cache" after changing app.php):
$models = config('app.models');
// foreach loop
You can prefix the name to a group of routes for the model
From the docs
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
Route::macro('crud', function () {
Route::name('{model}.')->group(function () {
Route::get('{model}', 'CrudController#index')->name('index');
Route::get('{model}/create', 'CrudController#create')->name('create');
Route::post('{model}', 'CrudController#store')->name('store');
Route::get('{model}/{id}', 'CrudController#show')->name('show');
Route::get('{model}/{id}/edit', 'CrudController#edit')->name('edit');
Route::match(['PUT', 'PATCH'], '{model}/{id}', 'CrudController#update')->name('update');
Route::delete('{model}/{id}', 'CrudController#destroy')->name('destroy');
});
});
}
}
Result
+-----------+----------------------------+-----------------+------------------------------------------------------------+
| Method | URI | Name | Action |
+-----------+----------------------------+-----------------+------------------------------------------------------------+
| GET|HEAD | {model} | {model}.index | App\Http\Controllers\CrudController#index |
| POST | {model} | {model}.store | App\Http\Controllers\CrudController#store |
| GET|HEAD | {model}/create | {model}.create | App\Http\Controllers\CrudController#create |
| GET|HEAD | {model}/{id} | {model}.show | App\Http\Controllers\CrudController#show |
| PUT|PATCH | {model}/{id} | {model}.update | App\Http\Controllers\CrudController#update |
| DELETE | {model}/{id} | {model}.destroy | App\Http\Controllers\CrudController#destroy |
| GET|HEAD | {model}/{id}/edit | {model}.edit | App\Http\Controllers\CrudController#edit |
+-----------+----------------------------+-----------------+------------------------------------------------------------+
Now you can access example.com/user/create and example.com/product/create and they both take to the same controller method
I hope this helps
So I found a few problems already which says that you have to override getAuthPassword() to give custom name of password column from database. Tried putting this method with the same name as column in a database and didnt work. It still shoots this error: Undefined index: password.
This is the auth:
if (Auth::attempt(Input::only('user_displayName'), Input::get('user_password')))
Tried changing user_password to password both in form and controller nothing works.
So the question is if I have a column in a database called "user_password" is there a way to make Auth work?
P.S checked every older solution I found
EDIT
Structure of user table:
+======================+
| User |
+======================+
| user_id |
+----------------------+
| user_displayName |
+----------------------+
| user_fname |
+----------------------+
| user_lname |
+----------------------+
| user_email |
+----------------------+
| user_password |
+----------------------+
| created_at |
+----------------------+
| updated_at |
+----------------------+
tldr; You can name your password field anything you like, as long as your User model implements the interface correctly.
However you can't pass different array key to the Auth::attempt method - only password index can be there
First off you're doing it wrong - you need to pass an array of credentials as 1st param:
if (Auth::attempt(Input::only('user_displayName', 'user_password')))
Next, unfortunately Eloquent provider has hard-coded password array index in the code, so you can't pass user_password to the attempt method.
So this is what you need:
$credentials = Input::only('user_displayName');
$credentials['password'] = Input::get('user_password');
if (Auth::attempt($credentials))
// or simply rename the input in your form to password and:
if (Auth::attempt(Input::only('user_displayName', 'password')))
I have not tested this but I believe you just have to override a function in UserTrait.php although don't hack the source. In your models/User.php file add the following function.
/**
* Get the password for the user.
*
* #return string
*/
public function getAuthPassword()
{
return $this->user_password;
}
There's a static solution for situations when implementing the given interface is not enough (e.g. you have two password columns, a user password and a support password).
$qry = DB::table('users')
->where('email', $email)
->whereNotNull('support_password')
->first();
if(!empty($qry))
{
$check = Hash::check($password, $qry->support_password);
if ($check)
{
Auth::loginUsingId($qry->id, true);
}
}
Here is my way of changing the default email field of the Laravel login to 'email_address' without changing the vendor code.
I made a trait class that extends the vendor AuthenticatesUsers trait. And only extended the username method.
App\Http\Controllers\Auth\AuthenticatesLogins.php:
namespace App\Http\Controllers\Auth;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
trait AuthenticatesLogins
{
use AuthenticatesUsers {
AuthenticatesUsers::username as parentUsername;
}
/**
* Get the login username to be used by the controller.
*
* #return string
*/
public function username()
{
$this->parentUsername();
return 'email_address';
}
}
Now in App\Http\Controllers\Controller\LoginController.php:
class LoginController extends Controller
{
use AuthenticatesLogins; // <-- for custom login fields
// etc.
And... done! Hope this helps.
I am trying to route to a RESTful controller using the following in app/routes.php:
Route::controller('register', 'RegisterController');
Route::get('/', 'HomeController#showWelcome');
In my app/controllers/RegisterController.php file I have added the following:
<?php
class RegisterController extends BaseController
{
public function getRegister()
{
return View::make('registration');
}
public function postRegister()
{
$data = Input::all();
$rules = array(
'first_name' => array('alpha', 'min:3'),
'last_name' => array('alpha', 'min:3'),
'company_name' => array('alpha_num'),
'phone_number' => 'regex:[0-9()\-]'
);
$validator = Validator::make($data, $rules);
if ($validator->passes()) {
return 'Data was saved.';
}
return Redirect::to('register')->withErrors($validator);
}
}
I am getting the following error:
Symfony \ Component \ HttpKernel \ Exception \ NotFoundHttpException
When I run php artisan routes in terminal I get:
+--------+--------------------------------------------------+------+----------------------------+----------------+---------------+
| Domain | URI | Name | Action | Before Filters | After Filters |
+--------+--------------------------------------------------+------+----------------------------+----------------+---------------+
| | GET /register/register/{v1}/{v2}/{v3}/{v4}/{v5} | | Register#getRegister | | |
| | POST /register/register/{v1}/{v2}/{v3}/{v4}/{v5} | | Register#postRegister | | |
| | GET /register/{_missing} | | Register#missingMethod | | |
| | GET / | | HomeController#showWelcome | | |
+--------+--------------------------------------------------+------+----------------------------+----------------+---------------+
I don't understand why register is showing twice in the URI and the second GET action is missing and why I am getting this error.
If you are using RESTful API, the best way is in your route,
Route::resource('register', 'RegisterController');
And change your public function getRegister() to public function index() and public function postRegister() to public function store()
Then the index() can be access using GET http://localhost/laravel/register and the store() using POST http://localhost/laravel/register
Chaneg the http://localhost/laravel/ with yours.
And the same way the update($id) is used for update and destroy($id) is used for delete
Route::controller('register', 'RegisterController');
This will also work if you change it
Route::controller('/', 'RegisterController');