This is the 4th part of the tutorial, and the source code will be available on Github, under the 0.4.0 tag.
Also check out the rest of the tutorial here:
Table of Contents
Images
One thing missing in our articles are images. We’ll now take a look at implementing image uploading and manipulation into our app.
You may have noticed in the beginning that we’re including the package imagine/Imagine. This package will handle our image manipulation. I chose this package since it supports both GD and Imagemagick. There are other packages available, and you’re free to use any other if you prefer.
Ok, so first let’s add a file upload element into our article form. Since we’re keeping things simple, I decided to use a Twitter Bootstrap fork that handles this element, and also offers a nice preview.
So go ahead and include [http://jasny.github.io/bootstrap/] into your backend layout.
One more thing I added is a service for the Imagine library. So I created the file app/services/Image.php. This is just a simple helper class that I like to use in my projects that resizes the images and caches them.
I won’t go into the complete functionality here, that’s why I wrote a separate post on my blog.
For now just include the helper into your app/services directory. I stripped out the comments, if you want to see everything checkout the code on Github.
app/services/Image.php
[code lang=”php”]
namespace App\Services;
use File, Log;
class Image {
protected $library = ‘imagick’;
protected $imagine;
public $overwrite = false;
public $quality = 85;
public function __construct($library = null)
{
if ( ! $this->imagine)
{
$this->library = $library ? $library : null;
if ( ! $this->library and class_exists(‘Imagick’)) $this->library = ‘imagick’;
else $this->library = ‘gd’;
if ($this->library == ‘imagick’) $this->imagine = new \Imagine\Imagick\Imagine();
elseif ($this->library == ‘gmagick’) $this->imagine = new \Imagine\Gmagick\Imagine();
elseif ($this->library == ‘gd’) $this->imagine = new \Imagine\Gd\Imagine();
else $this->imagine = new \Imagine\Gd\Imagine();
}
}
public function resize($url, $width = 100, $height = null, $crop = false, $quality = null)
{
if ($url)
{
$info = pathinfo($url);
if ( ! $height) $height = $width;
$quality = ($quality) ? $quality : $this->quality;
$fileName = $info[‘basename’];
$sourceDirPath = public_path() . $info[‘dirname’];
$sourceFilePath = $sourceDirPath . ‘/’ . $fileName;
$targetDirName = $width . ‘x’ . $height . ($crop ? ‘_crop’ : ”);
$targetDirPath = $sourceDirPath . ‘/’ . $targetDirName . ‘/’;
$targetFilePath = $targetDirPath . $fileName;
$targetUrl = asset($info[‘dirname’] . ‘/’ . $targetDirName . ‘/’ . $fileName);
try
{
if ( ! File::isDirectory($targetDirPath) and $targetDirPath) @File::makeDirectory($targetDirPath);
$size = new \Imagine\Image\Box($width, $height);
$mode = $crop ? \Imagine\Image\ImageInterface::THUMBNAIL_OUTBOUND : \Imagine\Image\ImageInterface::THUMBNAIL_INSET;
if ($this->overwrite or ! File::exists($targetFilePath) or (File::lastModified($targetFilePath) < File::lastModified($sourceFilePath)))
{
$this->imagine->open($sourceFilePath)
->thumbnail($size, $mode)
->save($targetFilePath, array(‘quality’ =&gt; $quality));
}
}
catch (\Exception $e)
{
Log::error(‘[IMAGE SERVICE] Failed to resize image &quot;’ . $url . ‘&quot; [‘ . $e->getMessage() . ‘]’);
}
return $targetUrl;
}
}
public function thumb($url, $width, $height = null)
{
return $this->resize($url, $width, $height, true);
}
public function upload($file, $dir = null)
{
if ($file)
{
// Generate random dir
if ( ! $dir) $dir = str_random(8);
// Get file info and try to move
$destination = public_path() . ‘/uploads/’ . $dir;
$filename = $file->getClientOriginalName();
$path = ‘/uploads/’ . $dir . ‘/’ . $filename;
$uploaded = $file->move($destination, $filename);
if ($uploaded) return $path;
}
}
}
[/code]
To use the class in a “Laravel” way (Image::resize()) we’ll also need a facade.
app/facades/ImageFacade.php
[code lang=”php”]
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class ImageFacade extends Facade {
protected static function getFacadeAccessor()
{
return new \App\Services\Image;
}
}
[/code]
Now we are ready to use out image helper. To autoload the facades we need to tell composer to do that, so make sure you have our custom directories in the autoloader classmap inside composer.json.
[code lang=”javascript”]
"app/services",
"app/facades",
"public/site"
[/code]
And don’t forget to run composer dump-autoload.
One last thing we’ll do is add an alias for the helper. And while we’re here, you can also add aliases for some other classes if you want.
app/config/app.php
[code lang=”php”]
‘Article’ => ‘App\Models\Article’,
‘Page’ => ‘App\Models\Page’,
‘Image’ => ‘App\Facades\ImageFacade’,
[/code]
Our image helper is ready now and after that we’ll add the input fields to our create and edit forms:
app/views/admin/articles/create.blade.php
[code lang=”php”]
…
<div class="control-group">
{{ Form::label(‘image’, ‘Image’) }}
<div class="fileupload fileupload-new" data-provides="fileupload">
<div class="fileupload-preview thumbnail" style="width: 200px; height: 150px;">
<img src="http://www.placehold.it/200×150/EFEFEF/AAAAAA&amp;text=no+image">
</div>
<div>
<span class="btn btn-file"><span class="fileupload-new">Select image</span><span class="fileupload-exists">Change</span>{{ Form::file(‘image’) }}</span>
</div>
</div>
</div>
…
[/code]
app/views/admin/articles/edit.blade.php
[code lang=”php”]
…
<div class="control-group">
{{ Form::label(‘image’, ‘Image’) }}
<div class="fileupload fileupload-new" data-provides="fileupload">
<div class="fileupload-preview thumbnail" style="width: 200px; height: 150px;">
@if ($article->image)
<a href="<?php echo $article->image; ?>"><img src="<?php echo Image::resize($article->image, 200, 150); ?>" alt=""></a>
@else
<img src="http://www.placehold.it/200×150/EFEFEF/AAAAAA&amp;text=no+image">
@endif
</div>
<div>
<span class="btn btn-file"><span class="fileupload-new">Select image</span><span class="fileupload-exists">Change</span>{{ Form::file(‘image’) }}</span>
<a href="#" class="btn fileupload-exists" data-dismiss="fileupload">Remove</a>
</div>
</div>
</div>
…
[/code]
Don’t worry much about the markup, this is the way the file input works with the Bootstrap fork. If you don’t like it, you’re free to use the native file input element, it should work also.
And not to forget the show view.
app/views/admin/articles/show.blade.php
[code lang=”php”]
@extends(‘admin._layouts.default’)
@section(‘main’)
<h2>Display article</h2>
<hr>
<h3>{{ $article->title }}</h3>
<h5>@{{ $article->created_at }}</h5>
{{ $article->body }}
@if ($article->image)
<hr>
<figure><img src="{{ Image::resize($article->image, 800, 600) }}" alt=""></figure>
@endif
@stop
[/code]
Our forms are ready, but they don’t do anything for now. We need to handle the upload inside our controller.
app/controllers/admin/ArticleController.php
[code lang=”php”]
// Your controller beginning…
public function store()
{
$validation = new ArticleValidator;
if ($validation->passes())
{
$article = new Article;
$article->title = Input::get(‘title’);
$article->slug = Str::slug(Input::get(‘title’));
$article->body = Input::get(‘body’);
$article->user_id = Sentry::getUser()->id;
$article->save();
// Now that we have the article ID we need to move the image
if (Input::hasFile(‘image’))
{
$article->image = Image::upload(Input::file(‘image’), ‘articles/’ . $article->id);
$article->save();
}
Notification::success(‘The article was saved.’);
return Redirect::route(‘admin.articles.edit’, $article->id);
}
return Redirect::back()->withInput()->withErrors($validation->errors);
}
public function update($id)
{
$validation = new ArticleValidator;
if ($validation->passes())
{
$article = Article::find($id);
$article->title = Input::get(‘title’);
$article->slug = Str::slug(Input::get(‘title’));
$article->body = Input::get(‘body’);
$article->user_id = Sentry::getUser()->id;
if (Input::hasFile(‘image’)) $article->image = Image::upload(Input::file(‘image’), ‘articles/’ . $article->id);
$article->save();
Notification::success(‘The article was saved.’);
return Redirect::route(‘admin.articles.edit’, $article->id);
}
return Redirect::back()->withInput()->withErrors($validation->errors);
}
// Rest of the controller code…
[/code]
As you can see we’re leveraging the upload() method from our Image helper. The images are uploaded to the public/uploads directory, so create it and make it writable. I know, this path is hard-coded into our helper. The reason for that is to speed up the tutorial, you can of course make a config file for the helper, and put all the paths there.
Now go ahead and try the image uploading, it should all work now if you followed everything correctly. If you have problems, take a look at the code on github. That code works 100%.
The theme
There’s really not much to this, so I wont go into the whole front-end stuff. We already have some code in place from the last part of the tutorial. So first just copy the entire public/site/assets directory from Github. After that include the assets in your views.
app/public/_partials/header.blade.php
[code lang=”php”]
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Laravel 4 Tutorial</title>
<link rel="stylesheet" href="{{ asset(‘site/assets/css/main.css’) }}">
</head>
<body>
<div id="layout">
<header id="header">
@include(‘site::_partials.navigation’)
<h1><a href="{{ route(‘home’) }}">Laravel 4 Tutorial</a></h1>
</header>
[/code]
app/public/_partials/navigation.blade.php
[code lang=”php”]
<nav id="nav">
<ul>
<li class="{{ (Route::is(‘home’)) ? ‘active’ : null }}"><a href="{{ route(‘home’) }}">Home</a></li>
<li class="{{ (Route::is(‘page’) and Request::segment(1) == ‘about-us’) ? ‘active’ : null }}"><a href="{{ route(‘page’, ‘about-us’) }}">About us</a></li>
<li class="{{ (Route::is(‘article.list’) or Route::is(‘article’)) ? ‘active’ : null }}"><a href="{{ route(‘article.list’) }}">Blog</a></li>
<li class="{{ (Route::is(‘page’) and Request::segment(1) == ‘contact’) ? ‘active’ : null }}"><a href="{{ route(‘page’, ‘contact’) }}">Contact</a></li>
</ul>
</nav>
[/code]
app/public/_partials/footer.blade.php
[code lang=”php”]
<footer id="footer">
<p>&copy; 2013 &bull; <a href="http://creolab.hr">Creo, Boris Strahija</a></p>
</footer>
</div><!–/#layout–>
</body>
</html>
[/code]
As you see I already prepared out site navigation and the active item should be marked. The theme itself is mod of the Modernist theme, but extremely simplified 🙂
The markup changed a bit since we also need to display images in our views.
app/public/site/views/index.blade.php
[code lang=”php”]
@include(‘site::_partials/header’)
{{ $entry->title }}
{{ $entry->body }}
@include(‘site::_partials/footer’)
[/code]
app/public/site/views/articles.blade.php
[code lang=”php”]
@include(‘site::_partials/header’)
<h2>Articles</h2>
<hr>
<ul class="articles">
@foreach ($entries as $entry)
<li>
<article>
@if ($entry->image)
<figure><a href="{{ route(‘article’, $entry->slug) }}"><img src="{{ Image::thumb($entry->image, 150) }}" alt=""></a></figure>
@endif
<h3><a href="{{ route(‘article’, $entry->slug) }}">{{ $entry->title }}</a></h3>
<h5>Created at {{ $entry->created_at }} &bull; by {{ $entry->author->email }}</h5>
<p>{{ Str::limit($entry->body, 100) }}</p>
<p><a href="{{ route(‘article’, $entry->slug) }}" class="more">Read more</a></p>
</article>
</li>
@endforeach
</ul>
@include(‘site::_partials/footer’)
[/code]
app/public/site/views/article.blade.php
[code lang=”php”]
@include(‘site::_partials/header’)
<article>
<h3>{{ $entry->title }}</h3>
<h5>Published at {{ $entry->created_at }} &bull; by {{ $entry->author->email }}</h5>
{{ $entry->body }}
<hr>
@if ($entry->image)
<figure><img src="{{ Image::resize($entry->image, 800, 600) }}" alt=""></figure>
<hr>
@endif
<a href="{{ route(‘article.list’) }}">&laquo; Back to articles</a>
</article>
@include(‘site::_partials/footer’)
[/code]
app/public/site/views/page.blade.php
[code lang=”php”]
@include(‘site::_partials/header’)
<article>
<h2>{{ $entry->title }}</h2>
{{ $entry->body }}
</article>
@include(‘site::_partials/footer’
[/code]
Error pages
There’s one thing missing, a nice 404 page. This is really easy to do in Laravel. We’ll need to modify our site routes.
public/site/routes.php
[code lang=”php”]
<?php
// Home page
Route::get(‘/’, array(‘as’ => ‘home’, function()
{
return View::make(‘site::index’)->with(‘entry’, Page::where(‘slug’, ‘welcome’)->first());
}));
// Article list
Route::get(‘blog’, array(‘as’ => ‘article.list’, function()
{
return View::make(‘site::articles’)->with(‘entries’, Article::orderBy(‘created_at’, ‘desc’)->get());
}));
// Single article
Route::get(‘blog/{slug}’, array(‘as’ => ‘article’, function($slug)
{
$article = Article::where(‘slug’, $slug)->first();
if ( ! $article) App::abort(404, ‘Article not found’);
return View::make(‘site::article’)->with(‘entry’, $article);
}));
// Single page
Route::get(‘{slug}’, array(‘as’ => ‘page’, function($slug)
{
$page = Page::where(‘slug’, $slug)->first();
if ( ! $page) App::abort(404, ‘Page not found’);
return View::make(‘site::page’)->with(‘entry’, $page);
}))->where(‘slug’, ‘^((?!admin).)*$’);
// 404 Page
App::missing(function($exception)
{
return Response::view(‘site::404’, array(), 404);
});
[/code]
As you can we added App::abort() to our routes if no entries are found. And at the end of our routes file we added a 404 handler with App::missing() that catches those errors and displays the custom 404 page. It doesn’t get any easier than that.
What’s next?
This time I’ll let you decide what topics I should cover next. So let me know in the comments…
very instructive, i learned alot from you bro, thx alot,
i would like you to cover if possible datatable/tabletool with laravel with permission,
i mean if user is admin, he get access to some links / buttons, he can delete, edit, create, if he’s moderator he can create , edit datatable, if he is simple user he can only insert.
is it doable ?
I like these tutorials as they are covering stuff other tutorials are leaving out – such as image upload. Thanks Boris!
@Propheteus If you want to see a good example of datatables and user permissions have a look at this:
https://github.com/andrew13/Laravel-4-Bootstrap-Starter-Site
Meanwhile I was waiting for your tutorial, I made a image uploading and manipulation with Intervention Image. The nice thing is that I made it with jquery file upload and for resizing and saving the image i just need one line.
What is your opinion? Which package is the best?
Thanks a lot for helping people with this tutorials. I became Laravel lover because of you.
Hi Boris,
first of all: Great tutorial! Thanks a lot!
In the last part I had some problems.
Maybe I overlooked it. But I had to add the following points to manage to upload an image:
1.) create an image config file (in app/config/image.php)
2.) use App\Services\Image; (in app/controllers/ArticlesController.php on top
3.) Instead of calling the image upload statically do it this way:
Old: $article->image = Image::upload(….
New:
$image = new Image();
$article->image = $image->upload(….
After these changes and addons the image upload also worked.
As I already said I’m not sure if I simply overlooked these points in your tutorial. If you forgot those, perhaps you can add them in the relevant paragraphs.
Hello,
Nice tutorial, probably the next step could be the “Category” management (add/manage category in article); what do you think?
Regards!
Accepting worldwide payment on a laravel website. That’s the next tutorial I would like to see.
Great work here – like what you have done.
If anyone is struggling with getting images to upload, don’t forget you manually have to set the enctype for your Form in your create and edit views. Simply add ‘enctype’=>’multipart/form-data’ to your array of Form options.
Right now I just wanna see a menu that isn’t hard coded and the index site being determined in the database, with an index value.
Thank you for saving my time.
This is enough to start project in laravel. Next we would like to see how to create packages.:) or some tips and tricks.;)
Thanks you very much for the great tutorial.
It will be great if you show us on how to create laravel package such as creating a blog or page package.
I’d say, add some cleaning up when deleting/editing Articles with an image.
When you delete or edit an article with an image. The (old) files stay there. Which will end up filling up the server.
I think it should be a standard part of this tutorial. Teach people to remove clutter as much as possible.
Great series, I am learning a lot.
I would like to suggest a completely new series that I would really like to see (and would even be willing to pay for) and feel a lot of people would benefit from. That is, a series on creating a multi-tenant SAAS application which would include creating new accounts, each of which would get their own subdomain, database and even a subscription plan that the account owner could change. Each account would have their own set of end users with various roles and permissions.
I have done something similar in ASP.NET MVC, but am very interested in learning how to architect a similar solution using Laravel.
Hi!
Thanks for all this turorials, I tried to upload multiples files based on your script but no look, I have changed the mysl database to text and erase the limit. Tried for each input::image in the form, but nothing….
Any idea?
Thanks!
Great tutorial series, thanks.
If you were going to expand on this, I personally could really benefit from further tutorials on setting up unit testing. You could set up a bunch of test for this project so people can build on this as a base for larger projects or something.
Hello! Excellent tutorial! But even when I downloaded the files from GitHub error persists “ErrorException Trying to get property of non-object”.
Any idea because it happens, or how I can fix it?
Just update in your users table the admin’s id to 1
Great article!
Add Unittests, Selenium tests(?).
Give us UML diagrams to every part.
Give us your ideas to alternate solution(s)…
In intro show what libraries will be used and why them.
Dont hardcode english like: ‘Article not found’. Show us how to use getLang().
Give us some more info about OOP/OOD. Why do You use: services for image, Fascade….
Best regards.
It’s unlikely I’ll do the testing part anytime soon, since I just started doing TDD recently, and I have a lot to lear. But I do recommend Jeffrey Ways excellent book on this topic. It’s really well written and makes it easy to get into TDD.
Hello Boris,
And thank you very much for this GREAT tutorial. I know you said it from the beginning that this is not a tutorial for the new ones to Laravel but here are my thoughts :
– the tutorial is well explained and yet hard to follow since some steps obvious to an experienced Laravel developer are not so obvious to your designated target readers (dump-autoload, registration of a service, the admins user id which increments even with truncate table command in place – very very strange thing…) .. you have to take into consideration that experienced Laravel programmers will probably only read the tutorial between the lines but never try to apply it because, oh well, they are experienced already and know how to do these things …
– another problem is that on github you have full sources and some of them contain code from part 4 which is not working ok if you are in the middle of part 3 for instance … perhaps you should include an archive for every part and we could compare easily where we did not follow correctly your instructions
– I could not understand either why you needed several assets folders, several script.js files … it really makes no sense … by the way, the github script.js lacks the modal window which is asking if you are sure you want to delete a record …
Even so I still think the tutorial is, as I said before, GREAT. For the future I am joining Lechus’s requests because many websites are requested with at least 2-3 languages options so we need to adapt. One other cool feature will be to take the interface to a new level and to show pages / articles in grids with options like filters, pagination and search.
Looking forward to see and learn new tricks from you,
Bogdan
1. The things you describe here are not Laravel specific. But I do mention quite a few time to run “composer dump-autolod”. I don’t really know what you mean by registering services. Maybe the facade? It is mentioned you need to autoload this directories.
As for the increment ID, again I don’t understand what you mean. The ID is set as an autoincrement field by default, this is just how the migration sets it up by default.
2. Github – every part of the tutorial is tagged, (0.2.0, 0.3.0, 0.4.0/0.4.1) so I don’t see an issue there. I’m definitely not planning to do a tutorial on checking out a specific tag from Github 😉
3. Again, this is personal preference. I tend to use Twitter Bootstrap for my projects, and like to organize my files this way, and later on when going into production I minify and combine the assets. It’s just my preference on organizing assest and has nothing to do with Laravel. If you don’t like, feel free to do this part your way. And what script do you mean lacks the modal window? Do you mean the confirm dialog? It’s in here: https://github.com/bstrahija/l4-site-tutorial/blob/master/public/assets/js/script.js.
Currently I don’t have a lot of time for tutorial, but I think the next part will cover multilanguage support, because I also think it’s very important, especially for non-english speaking developer that get this requirement on almost every project.
Great tutorial series! Thanks for going to the trouble of making it.
I might’ve missed it in the tutorial, but remember when uploading files to include ‘files’ => true in you array when opening the forms in your views:
{{ Form::open(array(‘route’ => ‘admin.articles.store’, ‘files’=> true)) }}
Also, in my controllers I had to call Image() like:
$image = new \App\Services\Image();
$article->image = image->upload(Input::file(‘image’), ‘articles/’ . $article->id);
As you can see i had to use the namespace, for some reason it couldn’t find it otherwise, I probably missed something somewhere – but hopefully this can help someone.
Thanks againBoris!
If you created the facade and registered the alias, then in your controllers (if they’re namespaced) just add an “use” statement above your class declaration:
use Image;
Or if you want you can reference the facade alias directly with a backslash: \Image::resize()
Regarding ‘files’ => true for your form, you’re right. It is in the code on Github and I’ll update the article accordingly.
Great tutorial so far, thanks a lot!
My suggestions for further topics:
– No static navigation view, but a dynamically generated one
– Managing pages with parent / child relations and generating URLs according to it
– A media library and adding images to content (wysiwyg editor maybe?)
Thanks! 🙂
Hi Adam,
I have worked out how to make the navigation go through all the pages.
in the routes.php where you have your Route::get(‘{slug}’, …)… You need to change the with clause to an array and remove the comma and replace with a =>. You then need to add a ‘pages’ => Page::all().
Here is the code on the with statement…
->with(array(‘pages’ => Page::all(), ‘entry’ => Page::where(‘slug’, $slug)->first()));
You can then do a foreach in your view and go @foreach($pages as page) then echo out the title etc…
Could talk about Laravel running on Google App Engine.
Google App Engine now runs up WordPress, I believe that to run Laravel 4 would not take much, just the same understanding of app.yaml.
Thanks for the series. A tutorial on:
* adding more than one images to a post
* add a html editor when creating the pages and articles
* multiple sites or blogs
* creating users with roles
Thanks again
Had a lot of issues with uploading an image…figured it out though:
You are calling the upload() method statically but you don’t have it declared that way…changing it to “public static function upload()” worked.
Still having issues with other functions…working those out atm….
Thanks for a great tutorial! I keep getting back to it.
I have one question though: How come You are placing the views outside the conventional view folder and into the public folder?
That’s a really nice suite of tutorials you published there. I hope you’ll release the next part about the multilanguage support !
Thanks for a lovely tutorial – I have made it through, and it was a
little above my level, oh well. I originally found the series because
of Sentry being involved – so if you were to make another tutorial, I think we’d be very keen to learn more about other things you can do with Sentry, multiple users that have different levels of access to different pages and different functionality
For anyone wondering about the static nav, I have worked it out…
in the routes.php where you have your Route::get(‘{slug}’, …)… You need to change the with clause to an array and remove the comma and replace with a =>. You then need to add a ‘pages’ => Page::all().
Here is the code on the with statement…
->with(array(‘pages’ => Page::all(), ‘entry’ => Page::where(‘slug’, $slug)->first()));
You can then do a foreach in your view and go @foreach($pages as page) then echo out the title etc…
Thanks! Very educational!
Could you help me please? I’ve got a problem with this.
I implemented your code, but when I want to upload image with
article, article creates/updates ok, but without image. My code is
exactly like yours, just changed from “article” to “post”
Comments are closed.