glossy
17 August 2015So 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.