Here is the middleware flow:
# Post edit
$this->get( '/edit/{id}/{slug}', \Rib\Src\Apps\Post\PostControllers\EditController::class . ':index' )
->add( new EnforceEditDelay() )
->add( new RequireOwner( 'posts' ) )
->add( new RejectBanned() )
->add( new RequireAuth() );
The ->add( new RejectBanned() ) cause the next middleware in the chain to break with:
'Call to a member function getArguments() on null'
RequireAuth():
class RequireAuth
{
# Variable used to disable redirect to '/user/set-username' from itelf. That would cause infinite redirection loop.
# This is passed to the middleWare from the list of routes. Of course only true for '/user/set-username' pages.
private $disableUserNameValidationCheck;
function __construct( $disableUserNameValidationCheck = false )
{
$this->disableUserNameValidationCheck = $disableUserNameValidationCheck;
}
public function __invoke( Request $request, Response $response, $next )
{
$session = $_SESSION;
# User is not authenticated: we ensure this by checking his id which is necessarily set when he is logged in.
if ( ! isset( $session[ 'id' ] ) ) {
FlashMessages::flashIt( 'message', "The page you tried to access requires that you are logged in the site." );
return $response->withRedirect( '/user/login' );
}
# In case user has logged in from a social network and has not set a user name and password. Username is 'temporary-.....'
# We really want the user to set his username. So on designated page we force redirect to page to setup username and email.
if ( ! $this->disableUserNameValidationCheck and isset( $session[ 'username' ] ) and strpos( $session[ 'username' ], 'temporary' ) !== false ) {
FlashMessages::flashIt( 'message',
"This part of the site requires that you complete your profile with a definitive username and email. Thank you for your understanding." );
return $response->withRedirect( '/user/set-username' );
}
$request = $request->withAttribute( 'session', $session );
# Process regular flow if not interrupted by the middleWare.
return $next( $request, $response );
}
}
RejectBanned():
class RejectBanned
{
/**
* Reject banned user
* #param Request $request
* #param Response $response
* #param $next
* #return Response
*/
public function __invoke( Request $request, Response $response, $next )
{
$session = $request->getAttribute( 'session' ) ?? null;
# Get usergroup from db
$user = ( new DbSql() )->db()->table( 'users' )->find( $session['id'] );
$userGroup = $user->user_group;
# Store it in session
$session['user_group'] = $userGroup;
# Redirect user if usergroup = banned
if ( $userGroup === 'banned' ) {
FlashMessages::flashIt( 'message', 'You are not allowed anymore to access this resource.' );
return $response->withRedirect( '/message' );
}
# Store info for the next middleware or controller
$request = $request->withAttributes( [ 'session' => $session ] );
# User is not banned, pursue
return $next( $request, $response );
}
}
RequireOwner() (this is where it breaks, I added a comment where it breaks):
class RequireOwner
{
private $table;
function __construct( $tableName )
{
$this->table = $tableName;
}
public function __invoke( Request $request, Response $response, $next )
{
$session = $request->getAttribute( 'session' ) ?? null;
// BREAKS HERE:
$recordId = $request->getAttribute( 'route' )->getArguments()[ 'id' ] ?? null; // BREAKS HERE
$currentUserGroup = $session[ 'user_group' ] ?? null;
$currentUserId = $session[ 'id' ] ?? null;
$recordInstance = ( new DbSql() )->db()->table( $this->table )->find( $recordId );
# If any info is missing, interrupt
if ( ! $recordInstance or ! $session or ! $recordId or ! $currentUserGroup or ! $currentUserId ) {
throw new Exception( 'Missing information to determine the owner of record' );
}
# Store info for the next middleware or controller
$request = $request->withAttributes( [ 'session' => $session, 'recordInstance' => $recordInstance ] );
# User is an Admin, he can edit any post
if ( $currentUserGroup === 'admin' ) {
return $next( $request, $response );
}
# User is not owner of post
if ( $currentUserId != $recordInstance->author_id ) {
FlashMessages::flashIt( 'message', 'You must be the author of this content to be able to edit it.' );
return $response->withRedirect( '/message' );
}
# User is not admin but is owner of content
return $next( $request, $response );
}
}
So why does the ->add( new RejectBanned() )causes the null value in the next middleware ?
In RejectBanned():
Changed
$request = $request->withAttributes( [ 'session' => $session ] );
to
$request = $request->withAttribute( 'session', $session );
And it fixed the issue.
Related
Sorry I feel like really stuck here.
I have a plugin introducing a new Rest API controller (WP_REST_Controller) with basically a single endpoint which uses a separate class as a client to fetch some data. Let's say:
#my_plugin.php
function register_items_routes() {
if ( ! class_exists( 'WP_REST_My_Controller' ) ) {
require_once __DIR__ . '/class-wp-my-controller.php';
}
$controller = new WP_REST_My_Controller();
$controller->register_routes();
}
add_action( 'rest_api_init', 'register_items_routes' );
_
#class-wp-my-controller.php
class WP_REST_My_Controller extends WP_REST_Controller {
/**
* Registers the routes.
*/
public function register_routes() {
$namespace = 'my/namespace';
$path = 'get-items';
register_rest_route( $namespace, '/' . $path, [
array(
'methods' => 'GET',
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' )
),
] );
}
public function get_items_permissions_check( $request ) {
return true;
}
/**
* Get items from My_Class and return them.
*
* #param WP_REST_Request $request The incoming HTTP request.
*
* #return WP_REST_Response|WP_Error The response containing the items in JSON, WP_Error in case of error.
*/
public function get_items( $request ) {
$client = new My_Class();
try {
$items = $client->fetch_some_items();
} catch ( Exception $e ) {
return new WP_Error(
'some-client-error',
$e->getMessage()
);
// Code to be tested. - Do some stuff with items and return.
return new WP_REST_Response( $items );
}
How am I supposed to stub the My_Class dependency from PhpUnit in order to return a predefined set of items which I could test with?
public function test_get_items() {
$request = new WP_REST_Request( 'GET', '/my/namespace/get-items' );
$data = rest_get_server()->dispatch( $request );
$expected_items = [
'some_key1' => 'some_value1',
'some_key2' => 'some_value2',
];
$this->assertTrue( count($data['items']) == count($expected_items) );
}
I have been working on a reset system for mails registered in my application.
when the user receives the mail and clicks on the link, he will be guided to my page where I receive token and email and his new password and reset for him his password.
return view($view, ['strings'=>$strings, 'email'=>$passwordReset->email])->with('token', $token);
as you can see here is the view i load to show to the user the password input.
the problem is i do not know how to send this token to the view. suppose i want it to be in an hidden input in my view.
the method which passes the token and email to the form looks like this
public function getReset($token = null)
{
$view = 'auth.web.reset2';
if ( is_null( $token ) ) {
throw new NotFoundHttpException;
}
$passwordReset = PasswordReset::getEmailFromToken( $token );
if ( is_null( $passwordReset ) ) {
throw new NotFoundHttpException;
}
$user = User::getUserFromEmail( $passwordReset->email );
if ( is_null( $user ) ) {
throw new NotFoundHttpException;
}
if(User::isUserMobileClient($user)){
//$view = 'auth.reset';
}
$strings = array(
trans(Strings::PASSWORD_RESET_BLADE_01),
trans(Strings::PASSWORD_RESET_BLADE_02),
trans(Strings::PASSWORD_RESET_BLADE_03),
trans(Strings::PASSWORD_RESET_BLADE_04),
trans(Strings::PASSWORD_RESET_BLADE_05),
trans(Strings::PASSWORD_RESET_BLADE_06),
trans(Strings::PASSWORD_RESET_BLADE_051),
trans(Strings::PASSWORD_RESET_BLADE_07),
trans(Strings::PASSWORD_RESET_BLADE_08),
);
return view($view,['strings'=>$strings, 'email'=>$passwordReset->email, 'token' => $token]);
}
and the method which is called when the user press the submit button is like this:
public function postReset(Request $request)
{
$userFromMobile = false;
$this->validate($request, [
'token' => 'required',
'email' => 'required|email',
'password' => 'required|confirmed|min:6',
]);
$credentials = $request->only(
'email', 'password', 'password_confirmation', 'token'
);
$response = Password::reset($credentials, function ($user, $password) {
$this->user;
$this->resetPassword($user, $password);
});
if(User::isUserMobileClient(User::getUserFromgEmail($request->email))){
$userFromMobile = true;
}
switch ($response) {
case Password::PASSWORD_RESET:
Input::flashonly('status');
return ($userFromMobile) ?
redirect('password/changed')->with('status', trans($response)) :
redirect($this->redirectPath())->with('status', trans($response));
default:
return redirect()->back()
->withInput($request->only('email'))
->withErrors(['email' => trans($response)]);
}
}
Why don't you just do this?
return view($view,['strings'=>$strings, 'email'=>$passwordReset->email, 'token' => $token]);
I have to work with the techcrunch wp-async-task to run a synchronization task in background in my wordpress plugin.
So to test, at the bottom of the main file I have :
//top of the php file
require_once(dirname(__FILE__) . '/lib/WP_Async_Task.php');
require_once(dirname(__FILE__) . '/class/my_api_status.class.php');
define('API_URL', '...');
/* ... */
// At the bottom of the file
function my_api_status($api_url)
{
sleep(5);
$r = wp_safe_remote_get($api_url);
if (!is_wp_error($r)) {
$body = json_decode(wp_remote_retrieve_body($r));
if (isset($body->success)) {
return;
}
}
}
add_action('wp_async_api_status', 'my_api_status');
function my_init_api_status()
{
new ApiStatusTask();
do_action('api_status', constant('API_URL'));
}
add_action('plugins_loaded', 'my_init_api_status');
And api status task class
class ApiStatusTask extends WP_Async_Task {
protected $action = 'api_status';
/**
* Prepare data for the asynchronous request
* #throws Exception If for any reason the request should not happen
* #param array $data An array of data sent to the hook
* #return array
*/
protected function prepare_data( $data ) {
return array(
'api_url' => $data[0]
);
}
/**
* Run the async task action
*/
protected function run_action() {
if(isset($_POST['api_url'])){
do_action("wp_async_$this->action", $_POST['api_url']);
}
}
}
The function prepare_data is correctly called by launchand after that launch_on_shutdown is also correctly called and finally wp_remote_post is called at the end of launch_on_shutdown with admin-post.php.
But the function run_action is never called ... and so the my_api_status in the main file.
What it possibly go wrong ?
I will put a complete example of a plugin here soon. But for now, I found my problem :
// In the `launch_on_shutdown` method of `WP_Async_Task` class
public function launch_on_shutdown() {
GcLogger::getLogger()->debug('WP_Async_Task::launch_on_shutdown');
if ( ! empty( $this->_body_data ) ) {
$cookies = array();
foreach ( $_COOKIE as $name => $value ) {
$cookies[] = "$name=" . urlencode( is_array( $value ) ? serialize( $value ) : $value );
}
$request_args = array(
'timeout' => 0.01,
'blocking' => false,
'sslverify' => false, //apply_filters( 'https_local_ssl_verify', true ),
'body' => $this->_body_data,
'headers' => array(
'cookie' => implode( '; ', $cookies ),
),
);
$url = admin_url( 'admin-post.php' );
GcLogger::getLogger()->debug('WP_Async_Task::launch_on_shutdown wp_remote_post');
wp_remote_post( $url, $request_args );
}
}
The sslverify option failed in my local environment. I just had to put it on false if we are not in production.
With this option set, the run_action is correctly trigger.
Right I have set up confide user authentication on my Laravel site.
I have ran everything as exactly as they said on the github page. When I direct myself to the user/create page I am presented with the form that I would normally posy me new info into. When I press submit I get this error on this url: /user.
On inspection these are the errors I get:
Symfony \ Component \ HttpKernel \ Exception \ NotFoundHttpException
Controller method not found.
* Handle calls to missing methods on the controller.
*
* #param array $parameters
* #return mixed
*/
public function missingMethod($parameters)
{
throw new NotFoundHttpException("Controller method not found.");
}
15. Symfony\Component\HttpKernel\Exception\NotFoundHttpException
…/vendor/laravel/framework/src/Illuminate/Routing/Controllers/Controller.php290
14. Illuminate\Routing\Controllers\Controller missingMethod
…/vendor/laravel/framework/src/Illuminate/Routing/Controllers/Controller.php302
13. Illuminate\Routing\Controllers\Controller __call
…/app/controllers/UserController.php42
12. User save
…/app/controllers/UserController.php42
11. UserController store
<#unknown>0
My UserController.php is setup like so:
<?php
/*
|--------------------------------------------------------------------------
| Confide Controller Template
|--------------------------------------------------------------------------
|
| This is the default Confide controller template for controlling user
| authentication. Feel free to change to your needs.
|
*/
class UserController extends BaseController {
/**
* Displays the form for account creation
*
*/
public function create()
{
return View::make(Config::get('confide::signup_form'));
}
/**
* Stores new account
*
*/
public function store()
{
$user = new User;
$user->username = Input::get( 'username' );
$user->email = Input::get( 'email' );
$user->password = Input::get( 'password' );
// The password confirmation will be removed from model
// before saving. This field will be used in Ardent's
// auto validation.
$user->password_confirmation = Input::get( 'password_confirmation' );
// Save if valid. Password field will be hashed before save
$user->save();
if ( $user->id )
{
// Redirect with success message, You may replace "Lang::get(..." for your custom message.
return Redirect::action('UserController#login')
->with( 'notice', Lang::get('confide::confide.alerts.account_created') );
}
else
{
// Get validation errors (see Ardent package)
$error = $user->errors()->all(':message');
return Redirect::action('UserController#create')
->withInput(Input::except('password'))
->with( 'error', $error );
}
}
/**
* Displays the login form
*
*/
public function login()
{
if( Confide::user() )
{
// If user is logged, redirect to internal
// page, change it to '/admin', '/dashboard' or something
return Redirect::to('/admin');
}
else
{
return View::make(Config::get('confide::login_form'));
}
}
public function do_login()
{
$input = array(
'email' => Input::get( 'email' ), // May be the username too
'username' => Input::get( 'email' ), // so we have to pass both
'password' => Input::get( 'password' ),
'remember' => Input::get( 'remember' ),
);
// If you wish to only allow login from confirmed users, call logAttempt
// with the second parameter as true.
// logAttempt will check if the 'email' perhaps is the username.
// Get the value from the config file instead of changing the controller
if ( Confide::logAttempt( $input, Config::get('confide::signup_confirm') ) )
{
// Redirect the user to the URL they were trying to access before
// caught by the authentication filter IE Redirect::guest('user/login').
// Otherwise fallback to '/'
// Fix pull #145
return Redirect::intended('/'); // change it to '/admin', '/dashboard' or something
}
else
{
$user = new User;
// Check if there was too many login attempts
if( Confide::isThrottled( $input ) )
{
$err_msg = Lang::get('confide::confide.alerts.too_many_attempts');
}
elseif( $user->checkUserExists( $input ) and ! $user->isConfirmed( $input ) )
{
$err_msg = Lang::get('confide::confide.alerts.not_confirmed');
}
else
{
$err_msg = Lang::get('confide::confide.alerts.wrong_credentials');
}
return Redirect::action('UserController#login')
->withInput(Input::except('password'))
->with( 'error', $err_msg );
}
}
public function confirm( $code )
{
if ( Confide::confirm( $code ) )
{
$notice_msg = Lang::get('confide::confide.alerts.confirmation');
return Redirect::action('UserController#login')
->with( 'notice', $notice_msg );
}
else
{
$error_msg = Lang::get('confide::confide.alerts.wrong_confirmation');
return Redirect::action('UserController#login')
->with( 'error', $error_msg );
}
}
public function forgot_password()
{
return View::make(Config::get('confide::forgot_password_form'));
}
public function do_forgot_password()
{
if( Confide::forgotPassword( Input::get( 'email' ) ) )
{
$notice_msg = Lang::get('confide::confide.alerts.password_forgot');
return Redirect::action('UserController#login')
->with( 'notice', $notice_msg );
}
else
{
$error_msg = Lang::get('confide::confide.alerts.wrong_password_forgot');
return Redirect::action('UserController#forgot_password')
->withInput()
->with( 'error', $error_msg );
}
}
public function reset_password( $token )
{
return View::make(Config::get('confide::reset_password_form'))
->with('token', $token);
}
public function do_reset_password()
{
$input = array(
'token'=>Input::get( 'token' ),
'password'=>Input::get( 'password' ),
'password_confirmation'=>Input::get( 'password_confirmation' ),
);
// By passing an array with the token, password and confirmation
if( Confide::resetPassword( $input ) )
{
$notice_msg = Lang::get('confide::confide.alerts.password_reset');
return Redirect::action('UserController#login')
->with( 'notice', $notice_msg );
}
else
{
$error_msg = Lang::get('confide::confide.alerts.wrong_password_reset');
return Redirect::action('UserController#reset_password', array('token'=>$input['token']))
->withInput()
->with( 'error', $error_msg );
}
}
public function logout()
{
Confide::logout();
return Redirect::to('/');
}
}
This is what the php artisan confide:controller creates for you and then you can do the same for routes which outputs this in the routes.php file for you:
// Confide routes
Route::get( 'user/create', 'UserController#create');
Route::post('user', 'UserController#store');
Route::get( 'user/login', 'UserController#login');
Route::post('user/login', 'UserController#do_login');
Route::get( 'user/confirm/{code}', 'UserController#confirm');
Route::get( 'user/forgot_password', 'UserController#forgot_password');
Route::post('user/forgot_password', 'UserController#do_forgot_password');
Route::get( 'user/reset_password/{token}', 'UserController#reset_password');
Route::post('user/reset_password', 'UserController#do_reset_password');
Route::get( 'user/logout', 'UserController#logout');
In my User.php model I have this setup which is normal:
<?php namespace App\Models;
use Eloquent;
use Illuminate\Auth\UserInterface;
use Illuminate\Auth\Reminders\RemindableInterface;
use Zizaco\Confide\ConfideUser;
use Zizaco\Entrust\HasRole;
class User extends ConfideUser {
use HasRole;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'users';
public function getPresenter()
{
return new UserPresenter($this);
}
/**
* Get user by username
* #param $username
* #return mixed
*/
public function getUserByUsername( $username )
{
return $this->where('username', '=', $username)->first();
}
/**
* Get the date the user was created.
*
* #return string
*/
public function joined()
{
return String::date(Carbon::createFromFormat('Y-n-j G:i:s', $this->created_at));
}
/**
* Save roles inputted from multiselect
* #param $inputRoles
*/
public function saveRoles($inputRoles)
{
if(! empty($inputRoles)) {
$this->roles()->sync($inputRoles);
} else {
$this->roles()->detach();
}
}
/**
* Returns user's current role ids only.
* #return array|bool
*/
public function currentRoleIds()
{
$roles = $this->roles;
$roleIds = false;
if( !empty( $roles ) ) {
$roleIds = array();
foreach( $roles as &$role )
{
$roleIds[] = $role->id;
}
}
return $roleIds;
}
/**
* Redirect after auth.
* If ifValid is set to true it will redirect a logged in user.
* #param $redirect
* #param bool $ifValid
* #return mixed
*/
public static function checkAuthAndRedirect($redirect, $ifValid=false)
{
// Get the user information
$user = Auth::user();
$redirectTo = false;
if(empty($user->id) && ! $ifValid) // Not logged in redirect, set session.
{
Session::put('loginRedirect', $redirect);
$redirectTo = Redirect::to('user/login')
->with( 'notice', Lang::get('user/user.login_first') );
}
elseif(!empty($user->id) && $ifValid) // Valid user, we want to redirect.
{
$redirectTo = Redirect::to($redirect);
}
return array($user, $redirectTo);
}
public function currentUser()
{
return (new Confide(new ConfideEloquentRepository()))->user();
}
}
So from this I can go to the form on /user/create and it outputs the form which means that route is working but on submit I get the No method error.
Can anyone shed some light onto this please?
Thanks
Whenever you type composer-dump autoload, composer recreates a bunch of files which tell it what classes should be registered into the autoloader. classmap autoloading requires you to composer dump-autoload whenever you make new files in a directory being autoloaded. psr-0 autoloading requires you to namespace your files but from then on you don't need to composer dump-autoload except for the first time you define the psr-0 autoloading in your composer.json file.
I have a rest controller example im trying to run that is giving me a headache.
My url im trying to access is localhost/books/edit/1
For some weird reason this route seems to call the getAction with the Controller instead of the editAction. And it throws errors saying that the object doesnt exist.
The controller is,
class BooksController extends Zend_Rest_Controller {
private $_booksTable;
private $_form;
public function init() {
$bootstrap = $this->getInvokeArg ( 'bootstrap' );
$db = $bootstrap->getResource ( 'db' );
$options = $bootstrap->getOption ( 'resources' );
$dbFile = $options ['db'] ['params'] ['dbname'];
if (! file_exists ( $dbFile )) {
$createTable = "CREATE TABLE IF NOT EXISTS books (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name VARCHAR(32) NOT NULL,
price DECIMAL(5,2) NOT NULL
)";
$db->query ( $createTable );
$insert1 = "INSERT INTO books (name, price) VALUES ('jQuery in Action', 39.99)";
$insert2 = "INSERT INTO books (name, price) VALUES ('PHP in Action', 45.99)";
$db->query ( $insert1 );
$db->query ( $insert2 );
}
$this->_booksTable = new Zend_Db_Table ( 'books' );
$this->_form = new Default_Form_Book ();
}
/**
* The index action handles index/list requests; it should respond with a
* list of the requested resources.
*/
public function indexAction() {
$this->view->books = $this->_booksTable->fetchAll ();
}
/**
* The list action is the default for the rest controller
* Forward to index
*/
public function listAction() {
$this->_forward ( 'index' );
}
/**
* The get action handles GET requests and receives an 'id' parameter; it
* should respond with the server resource state of the resource identified
* by the 'id' value.
*/
public function getAction() {
$this->view->book = $this->_booksTable->find ( $this->_getParam ( 'id' ) )->current ();
}
/**
* Show the new book form
*/
public function newAction() {
$this->view->form = $this->_form;
}
/**
* The post action handles POST requests; it should accept and digest a
* POSTed resource representation and persist the resource state.
*/
public function postAction() {
if ($this->_form->isValid ( $this->_request->getParams () )) {
$this->_booksTable->createRow ( $this->_form->getValues () )->save ();
$this->_redirect ( 'books' );
} else {
$this->view->form = $this->_form;
$this->render ( 'new' );
}
}
/**
* Show the edit book form. Url format: /books/edit/2
*/
public function editAction() {
var_dump ($this->getRequest()->getParam ( 'edit' ));
$book = $this->_booksTable->find ( $this->getRequest()->getParam ( 'id' ) )->current ();
var_dump ($book->toArray ());
$this->_form->populate ( $book->toArray () );
$this->view->form = $this->_form;
$this->view->book = $book;
}
/**
* The put action handles PUT requests and receives an 'id' parameter; it
* should update the server resource state of the resource identified by
* the 'id' value.
*/
public function putAction() {
$book = $this->_booksTable->find ( $this->_getParam ( 'id' ) )->current ();
if ($this->_form->isValid ( $this->_request->getParams () )) {
$book->setFromArray ( $this->_form->getValues () )->save ();
$this->_redirect ( 'books' );
} else {
$this->view->book = $book;
$this->view->form = $this->_form;
$this->render ( 'edit' );
}
}
/**
* The delete action handles DELETE requests and receives an 'id'
* parameter; it should update the server resource state of the resource
* identified by the 'id' value.
*/
public function deleteAction() {
$book = $this->_booksTable->find ( $this->_getParam ( 'id' ) )->current ();
$book->delete ();
$this->_redirect ( 'books' );
}
}
The bootstrap is,
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap {
protected function _initAutoload() {
$autoloader = new Zend_Application_Module_Autoloader ( array (
'namespace' => 'Default_',
'basePath' => dirname ( __FILE__ )
) );
return $autoloader;
}
protected function _initRestRoute() {
$this->bootstrap ( 'Request' );
$front = $this->getResource ( 'FrontController' );
$restRoute = new Zend_Rest_Route ( $front, array (), array (
'default' => array ('books' )
) );
$front->getRouter ()->addRoute ( 'rest', $restRoute );
}
protected function _initRequest() {
$this->bootstrap ( 'FrontController' );
$front = $this->getResource ( 'FrontController' );
$request = $front->getRequest ();
if (null === $front->getRequest ()) {
$request = new Zend_Controller_Request_Http ();
$front->setRequest ( $request );
}
return $request;
}
}
Can anyone see what might be causing the getAction to be called when browsing to that link ???
edit should follow the identifier, so the correct edit URL is http://localhost/books/1/edit