Unlikenesses A Backend Developer

glossy

17 August 2015

So my "learning Laravel" project is currently a simple app that will allow the user to keep a log of glosses / annotations to books they're reading. Basically a simple CRUD app, with the ability to add Authors, Books and Annotations to those books. This kind of thing would take me half an hour in CodeIgniter, but since I'm new to Laravel it's taking a lot longer here, which is fine and to be expected.

So I just wanted to keep track here of a few little bumps I encounter along the way and the way I solve them. The kind of things which are probably incredibly simple but which, coming to this framework for the first time, had me stumped until I found the solution.

The first was using the list database method. I wanted to pass the view a list of authors to put in a dropdown. Following the Laracasts tutorials this seemed the best way to pass an array of values from a single column to populate a dropdown in a view. My first problem was that my authors table only has first_name and last_name columns. I want both to show in the dropdown, but I can't pass them both in the list method.

The answer was mutators. Following the convention in the manual (get + camel-cased version of name + Attribute), I created a function getFullNameAttribute which simply returned $this->first_name.' '.$this->last_name. This gave me a full_name attribute which I could pass to lists. My create function in the BooksController now had this line:

$authors = \App\Author::lists('full_name','id');

However, there was another problem. This simply didn't work - the dropdown was unpopulated, or rather, it was populated with two blank lines. Doing a die and dump (dd($authors)) showed that an array was being passed to it, but an array of two strings consisting of just one space: " ". The solution, I found after some googling, was to call all() before calling the method lists. The final, working line is

$authors = \App\Author::all()->lists('full_name','id');

It's a shame that I can't explain why this works yet. I think I need to delve deeper into Laravel before I'll be able to do that. But for now, it's working.

Another great thing I discovered about Laravel was the way it deals with foreign keys and deletion. In my migration for creating the Books table, I set the author_id to be a foreign key. This is done using (as Jeffrey Way points out in his tutorial) a brilliantly intuitive syntax:

$table->foreign('author_id')->references('id')->on('authors');

The same syntax can be used to specify its behaviour when an author is deleted. Adding onDelete('cascade') to the chain means that if an author is deleted, all his books will be deleted too. Actually I didn't know about the MySQL cascade action before coming across this feature in Laravel. This is a great function which will save me loads of time in the future.

The next problem I face is the creation of a route that allows me to view sub-tables. E.g. books has a foreign key, author_id, and I want to be able to create a route which allows me to see only the books which have an author_id of, say, 2. The problem is that my routes for books are set up using the resource method:

Route::resource('books','BooksController');

which, as the docs say is used for creating multiple routes to various REST actions; but for me it's also just a quick way to set up the default CRUD routes I need. So I have routes like books/create or books/1/edit (I'm using ids rather than slugs at the moment). But what I need is a URL like authors/2/books/create -- in other words, the ability to add, view, and edit books for a specific author. For the moment, I have a new route:

Route::get('authors/{id}/books','BooksController@showBooksByAuthor');

Then in booksController I have a simple function showBooksByAuthor, that takes the proffered id and grabs all the books by that author, before loading the books.index page. This is not ideal though - I feel like I'm polluting both the routes file and the simplicity of the controller, but I don't yet know how to fix that.

[Time passes...]

Ok, a couple of hours later, I have the solution: nested resources. I replace

Route::resource('books','BooksController');

with

Route::resource('authors.books','BooksController');

and hey presto, the URL system is set up precisely as I envisaged it. It's almost as if Laravel read my mind. Some changes have had to be made. For instance, I realised that when opening the form for creating new books, one has to pass the author_id in:

Form::open(['route'=>array('authors.books.store',$author->id)])

But for me to do that I had to grab the author -- where could I do that? It took me some searching to figure out that with nested routes the URL variables are passed as arguments to the controller methods. So in my create method in the booksController I could just grab the author as an argument and send it to the create view.

public function create(Author $author)
{
    return view('books.create',array('author'=>$author));
}

I can then add the same argument to the store method:

public function store(Requests\BookRequest $request, Author $author)

The next problem is that even if I pass the $author to the store function, I still need to extract the author_id and add it to the $request. I solve this problem by adding the author_id to the $request array: $request['author_id'] = $author->id;. It doesn't seem elegant but it works for now.