Laravel 5 socialite with Facebook integration

Would you like to offer a Facebook login functionality next to a regular e-mail based login? This is a tutorial to achieve that with Laravel 5 and the Socialite plugin. This tutorial is based on Matt Stauffer’s tutorial.

First of all pull in Laravel Socialite via composer:

composer require laravel/socialite

Create the users and password_remember migration:

$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->string('avatar');
$table->string('password', 60);
$table->boolean('is_admin');
$table->string('facebook_id');
$table->rememberToken();
$table->timestamps();

Get your Facebook developer id and secret at: https://developers.facebook.com/.

Insert the Facebook credentials into the app/services.php file:

'facebook' => [
        'client_id' => env('FACEBOOK_ID'),
        'client_secret' => env('FACEBOOK_SECRET'),
        'redirect' => env('FACEBOOK_URL'),
    ],

In my case, I store them in the .env file as environment variables:

FACEBOOK_ID=xxx
FACEBOOK_SECRET=yyy
FACEBOOK_URL=http://myapp.devapp/auth/facebook/callback

Create a users model and make sure that some fields are fillable:

<?php

namespace App;

use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{
    use Authenticatable, CanResetPassword;

    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'users';

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name', 'email', 'password', 'avatar', 'facebook_id'];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = ['password', 'remember_token'];
}

So the model and migrations are prepared, if necessary, run your migration:

php artisan migrate

Register a new controller in your routes file (if there’s an auth controller already, do it above the auth controller):

Route::get('/auth/facebook', 'Auth\SocialController@redirectToProvider');
Route::get('/auth/facebook/callback', 'Auth\SocialController@handleProviderCallback');

Create the Facebook Social Auth Controller (app/Http/Controllers/Auth/SocialController.php):

<?php namespace App\Http\Controllers\Auth; use Illuminate\Http\Request; use App\User; use App\Http\Requests; use App\Http\Controllers\Controller; use Auth; use Socialite; class SocialController extends Controller { /** * Redirect the user to the Facebook authentication page. * * @return Response */ public function redirectToProvider() { return Socialite::driver('facebook')->redirect();
    }

    /**
     * Obtain the user information from Facebook.
     *
     * @return Response
     */
    public function handleProviderCallback()
    {
        $user = Socialite::driver('facebook')->user();

        $authUser = $this->findOrCreateUser($user);

        Auth::login($authUser, true);

        return redirect()->back();
    }

    /**
     * Return user if exists; create and return if doesn't
     *
     * @param $fbUser
     * @return User
     */
    private function findOrCreateUser($fbUser)
    {

        if ($authUser = User::where('facebook_id', $fbUser->id)->first()) {
            return $authUser;
        }

        return User::create([
            'name' => $fbUser->name,
            'email' => $fbUser->email,
            'facebook_id' => $fbUser->id,
            'avatar' => $fbUser->avatar
        ]);

    }
}

You can now link to your social auth controller from somewhere in your blade view:

<a class="btn btn-primary" href="{{ action('Auth\SocialController@redirectToProvider') }}"> Login with Facebook</a>

Important safety note

If you’d like to keep the possibility for people to login, make sure that you’ve empty password validation checks, so that people can’t sign in with only Facebook e-mail addresses.

That’s it. Do you’ve additions to this tutorial? Let me know in the comments.

Validating meta title and meta description with jQuery

Would you like to auto-display validation hints when a meta_title field is longer than 55 chars and a meta_description field is longer than 155 chars?

Then you can use this package: jQuery Meta Title and Description Validation.

Setup
1. Have at least one input field named meta_title or meta_description

E.g.

<p>
<input type="text" name="meta_title" placeholder="Meta title">
</p>
<p>
<input type="text" name="meta_description" placeholder="Meta description">
</p>

2. Initialize by including jquery-meta-validation.js

<script type="text/javascript" src="js/jquery-meta-validation.js"></script>

Laravel 5 Admin Middleware (is_admin user check)

Would you like to have middleware that makes sure that only users with an is_admin = 1 status will be able to see the admin section? Then you can use the following code:

app/Http/Middleware/AdminMiddleware.php

<?php

namespace App\Http\Middleware;

use Closure;

class AdminMiddleware
{

    /**
     * Handle an incoming request. User must be logged in to do admin check
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if (\Auth::user()->is_admin == 1)
        {
            return $next($request);
        }

        return redirect()->guest('/');
    }
}

Make sure you’ll register the middleware as a route in app/Http/Kernel.php

protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'admin' => \App\Http\Middleware\AdminMiddleware::class
];

Finally assign this middleware in the routes.php next to the auth middleware, since the admin middleware is an extension to the auth middleware.

App/Http/routes.php

Route::group(['prefix' => 'administration', 'middleware' => ['auth', 'admin']], function()
{
	Route::get('/', 'Admin\HomeController@index');
}

If you’ve suggestions to make this coding more efficient, you’re always welcome to drop a comment below.

Magento sales transactional email – highlight if billing address equals shipping address

Insert this code on the desired place in the .html transactional email (deepter in the locale folder):

{{block type='core/template' area='frontend' template='email/shippingcheck.phtml' order=$order}}

Then create the file in the template you’re using (app/design/frontend/default/default/template/email/shippingcheck.phtml) for example. Insert the following content:

<?php if($this->getData('order')->getShippingAddress()->format('html') != $this->getData('order')->getBillingAddress()->format('html')) { echo $this->getData('order')->getShippingAddress()->format('html'); } else { echo 'The billing address equals the shipping address.'; } ?>

Backup Ubuntu site / Laravel Forge server with Tarsnap

Would you like to backup your Ubuntu server with Tarsnap and also backup your Mysql database? Then follow this steps:

Install Tarsnap

1. Install dependencies

sudo apt-get install build-essential ext2fs-dev zlib1g-dev libssl-dev 

2. Install Tarsnap

Download Tarsnap with this wget:

wget --no-check-certificate https://www.tarsnap.com/download/tarsnap-autoconf-1.0.35.tgz

Now we need to extract, configure, and compile Tarsnap.

tar xfz tarsnap-autoconf-1.0.35.tgz
cd tarsnap-autoconf-1.0.35
./configure
sudo make install clean

3. Configure Tarsnap

Copy the example config to the live config:

sudo mv /usr/local/etc/tarsnap.conf.sample /usr/local/etc/tarsnap.conf

Then create a key, save this key to a USB or something, you can’t reset it.

mkdir ~/.tarsnap
tarsnap-keygen --keyfile /home/youruser/.tarsnap/tarsnap.key --user your@email.com --machine your-machine-name
You’ll be prompted for your Tarsnap password when running tarsnap-keygen.

Now, edit the tarsnap.conf file:

sudo pico /usr/local/etc/tarsnap.conf

Point the keyfile directive to the key file we created a couple steps ago. The top of your tarsnap.conf file should look similar to this now:

### Recommended options
 
# Tarsnap cache directory
cachedir /tmp/tarsnap-cache
 
# Tarsnap key file
keyfile /home/youruser/.tarsnap/tarsnap.key

4. Use Tarsnap to Make a Backup

To create a backup do this:

tarsnap -c -f servername-20140805 /home/forge

I’m using Laravel forge’s home directory. This directory is now backup up to Tarsnap. In the next step we’ll add a backup folder with the latest mysql-database.

Backup Mysql database

Create a folder in /home/forge : backup. Create a file in the folder:

touch /home/forge/makebackup.sh

Insert this information in the file:

#!/bin/sh
TODAY=$(date "+%A");
flock -n ~/.mysqldump mysqldump -u forge -p forge --password=YOURPASSWORD > ~/backup/mysql-latest.sql; 
flock -n ~/.tarsnap.lock tarsnap -c -f "$(uname -n)-$(date +%Y-%m-%d_%H-%M-%S)" ~/ 2>/dev/null; 

Create Cronjob

Let the makebackup.sh be called every x days:

crontab -e

Insert this piece of info:

PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
MAILTO={{YOUREMAILHERE}}
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
0 2 * * * flock -n ~/.makebackup.lock ~/backup/makebackup.sh

To view the backupped files so far:

tarsnap --list-archives | sort

Thanks to Longren.io and thanks to Hypernode

Laravel 5: simple multi-tenant/multi-site model setup (many-to-many relation covered as well)

Laravel multi site multi tenant navigation bar illusatrationLaravel 5 is a brand new version of the popular framework. Unfortunately, there are no good multi-tenant setups for Laravel 5 yet. That’s why I’d decided to create my own simple version as well, based on Eloquent Global Scopes.

In my case I wanted a sites table, with categories that have a many to many relationship. So like:

sites
id | name

categories
id | name

site_category
id | site_id | category_id

So this table structure is a many to many relation. With this setup you can create a multi-site setup with different categories. Site A has categories 1 and 2, Site B has category 3 etc. It’s defined in the model as follows:

App\Site.php

<?php namespace App;

use Illuminate\Database\Eloquent\Model;

class Site extends Model {

    protected $fillable = ['name', 'domain'];

    public function categories()
    {
        return $this->belongsToMany('App\Category', 'site_category');
    }
}

And the category like:

App\Category.php

<?php namespace App;

use Illuminate\Database\Eloquent\Model;
use App\Traits\TenantableTrait;

class Category extends Model {
	use TenantableTrait;

	/**
	 * The fillables
	 * @var array
	 */
	protected $fillable = ['name', 'description'];

	/**
	 * A category has many sites
	 */
	public function sites()
    {
        return $this->belongsToMany('App\Site', 'site_category');
    }

Notice that this category has the trait TenantableTrait assigned. That is like:

App\Traits\TenantableTrait.php

<?php namespace App\Traits;

use App\TenantScope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;

trait TenantableTrait {

    /**
     * Boot the tenantable trait for the model
     *
     * @return void
     */
    public static function bootTenantableTrait()
    {
        static::addGlobalScope(new TenantScope);
    }

}

And finally, don’t forget the global Eloquent scope:

App\TenantScope.php

&lt;?php namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\ScopeInterface;
use Session;

class TenantScope implements ScopeInterface
{
    /**
     * Apply the scope to a given Eloquent query builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return void
     */
    public function apply(Builder $builder, Model $model)
    {
        if (Session::has('siteId'))
        {
            $siteId = session('siteId');
          
            $builder->whereHas('sites', function($query) use($siteId)
            {
                $query->where('sites.id', $siteId);
            });
        }
    }

    /**
     * Remove the scope from the given Eloquent query builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return void
     */
    public function remove(Builder $builder, Model $model)
    {
        
        dd('remove called');
    }
}

Now, if you set the site id by session (e.g.) in a controller with:

Session::set('siteId', 1)

A all categories that have a site_category link with site_id=1 will be called. As you see, the remove method still has to be specified, so if you’d like to finish up this script, leave it in the comments.

You can repeat this with site_products or probably even site_category_product (with some finetuning, if that works, let me know below 🙂 ).

Magento remove images of deleted products

Clean up not used Magento images

Magento unfortunately does not remove images of a product unfortunately if the product is removed. The following script checks all the images and checks if it’s still present in the database of Magento. If not, it deletes the image. Use the following code:

<?php

$mageFilename = 'app/Mage.php';
require_once $mageFilename;

Mage::setIsDeveloperMode(true);
ini_set('display_errors', 1);
umask(0);

Mage::app()->setCurrentStore(Mage_Core_Model_App::ADMIN_STORE_ID);

set_time_limit(0);
ini_set('memory_limit','1024M');

$media = Mage::getBaseDir('media').'/catalog/product';

echo "Query database for images …\n";
$query = "SELECT value FROM catalog_product_entity_media_gallery";
$data = Mage::getSingleton('core/resource')->getConnection('core_read')->fetchAll($query);

$dbData=array();
foreach($data as $item){
  $dbData[$item['value']]=$item['value'];
}

echo "Images found in database:".count($dbData)."\n";

echo "Search images in media directory …\n";
$images = findFiles($media, array('jpg'));
echo "Images found under directory ($media):".count($images['jpg'])."\n";

echo "Start removing images …\n";
$removedCount=0;
$skippedCount=0;
foreach($images['jpg'] as $image) {
  if(strpos($image,'cache')!==false)
  {
    //echo "Skip cached image : $image\n";
    continue;
  }

  $imageCleanup = str_replace($media,"",$image);
  if(isset($dbData[$imageCleanup]))
  {
    echo "Skip image is in database : $image\n";
    $skippedCount++;
    continue;
  }
  else
  {
    echo "Remove image : $image\n";
    //if($testrun==false) unlink($image);
    $removedCount++;
  }
}

echo "Done, removed $removedCount images and skipped $skippedCount images.\n";

function findFiles($directory, $extensions = array()) {
  function glob_recursive($directory, &$directories = array()) {
    foreach(glob($directory, GLOB_ONLYDIR | GLOB_NOSORT) as $folder) {
      $directories[] = $folder;
      glob_recursive("{$folder}/*", $directories);
    }
  }
  glob_recursive($directory, $directories);
  $files = array ();
  foreach($directories as $directory) {
  foreach($extensions as $extension) {
  foreach(glob("{$directory}/*.{$extension}") as $file) {
  $files[$extension][] = $file;
}
}
}
return $files;
}

?>

Clean up Magento space: could save a lot of GBs

In my case, I tried the script for a store with 40K images of which 10K were not used. This is a disk space reduction of a few Gigabytes.

Final word

This script is thanks to a comment on RapidCommerce. If you use the script, that’s on your own responsibility. Try it out on a staging server and make a backup in advance. Cheers!

Load normal product price instead of indexed product price

Would you like to ignore the indexed product price in the templates (for example in catalog/product/list.phtml where the indexed price is used)?

Then go to: catalog/product/list.phtml

Change:

<?php echo $this->getPriceHtml($product) ?>

With:

<?php $product = Mage::getModel( 'catalog/product' )->load($_product->getId()); ?>
<?php echo $this->getPriceHtml($product) ?>

There are two occurrences in list.phtml, so don’t forget to replace it twice. Only do this when it’s the only solution of displaying right prices, since loading the product takes extra time, compared to loading the indexed product price.

Magento Contact Form Attachment Functionality

Would you like to have attachments in your contact form in Magento?

Do the following:

  1. Clone the zip of this github: https://github.com/yogisfunda/magento-contact-form-attachment
  2. Upload (merge) the files into the public_html directory
  3. Recompile/flush cache
  4. Go to your-site.com/contacts
  5. If the form does not changes or there’s a blank page, move the contents of the frontend/default part to frontend/base.

You can insert the form as a widget on a cms page by adding the following code:

{{block type="core/template" name="contactForm" form_action="/contacts/index/post" template="eurowsport/customform/customform.phtml"}}