Selecting a team via the URL in Laravel Spark

08 Dec 2016 in Tech

Spark is an awesome way to get started writing applications without needing to build in user authentication, team management and payment processing. The application I'm working on at the moment is team focused, so I wanted to charge a team rather than individual users. I've got a feeling that most of the applications I write are going to be centered around team billing, so here's a one stop guide to configuring Spark for teams.

Bootstrap your application

bash
composer global require laravel/installer
sudo npm -g install gulp
spark new -n --team-billing APP_NAME

Configure your environment

Create a MySQL user and database for the application, then edit the .env file with these credentials and run database migrations

bash
export DB_NAME=myapp
export DB_USER=myapp
export DB_PASS=somepassword
mysql -u root -e "GRANT ALL on $DB_NAME.* TO $DB_USER@'localhost' IDENTIFIED BY '$DB_PASS';"
mysql -u root -e "CREATE DATABASE myapp;"
sed -i "s/DB_DATABASE=.*/DB_DATABASE=$DB_NAME/" .env
sed -i "s/DB_USERNAME=.*/DB_USERNAME=$DB_USER/" .env
sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=$DB_PASS/" .env
php artisan migrate

Enable accessing teams by path

By default, Spark uses the session to know which team you're looking at. I'm not a fan of this as it means a link can have different meanings depending on your session state. I prefer having all the info in the URL so that you can share links with team mates. Fortunately, Spark also supports this.

Edit ./app/Providers/SparkServiceProvider.php and add the following to the booted method:

php
Spark::identifyTeamsByPath();

Whilst you're in there, you probably want to add your email address to the $developers class parameter.

At this point, you should be able to run php artisan serve and visit http://localhost:8000/ to register an account (I've called my first team Demo). Once you've registered and logged in, immediately click on the logo in the top right and register another team (this one's called Something).

Getting the team from the URL

To get the team from the URL, you'll need to set up a route to capture the current team. Open up routes/web.php and take a look at the home route. We're going to update it so that it contains the team slug:

php
Route::get('/{team_slug}/home', 'HomeController@show');

Next, we'll update app/Http/Controllers/HomeController.php so that it outputs the name of the current team. Add the following on line 28:

php
echo \Auth::user()->currentTeam->name;
exit;

Finally, visit the page in a browser. We changed the route so our normal /home URL won't work any more. Instead we need to visit http://localhost:8000/demo/home. Hopefully you'll see the same of your current team on the screen. If we change the URL to http://localhost:8000/something/home I'd expect the team name to change, but it doesn't. We have to handle detecting the team ourselves.

Changing team automatically

Changing the team that we're on automatically is a job for middleware! At this point we're going to be writing code so we need to create a directory for it and update the Composer autoloader so that it knows where to find it.

bash
mkdir -p src/Middleware
jq '.autoload["psr-4"]["mheap\\"] = "src"' composer.json > c2.json
mv c2.json composer.json
composer dump-autoload
touch src/Middleware/FetchCurrentTeamFromUrl.php

Edit src/Middleware/FetchCurrentTeamFromUrl.php and add the following contents (I share the current team's slug with all views too):

php
<?php
namespace mheap\Middleware;
use App;
use Closure;
use Exception;
use Route;
use View;
class FetchCurrentTeamFromUrl {
public function handle($request, Closure $next)
{
$params = Route::current()->parameters();
if (!isset($params['team_slug'])) {
throw new Exception('No team set, but one was expected');
}
$team = App\Team::where('slug', $params['team_slug'])->first();
abort_unless($request->user()->onTeam($team), 404);
$request->user()->switchToTeam($team);
View::share('currentTeamSlug', $team->slug);
return $next($request);
}
}

Update app/Http/Kernel.php and add the following to $routeMiddleware:

php
'fetchTeam' => \mheap\Middleware\FetchCurrentTeamFromUrl::class,

Finally, we need to enable this middleware on our routes. Open up routes/web.php again and replace the line that points to the home controller with the following:

php
Route::group(['prefix' => '{team_slug}', 'middleware' => ['auth', 'fetchTeam']], function(){
Route::get('/home', 'HomeController@show')->name('home');
});

Note how we named the route - that will be important later. Now, if we visit http://localhost:8000/demo/home it should say "Demo", and if we visit http://localhost:8000/something/home it should say "Something". We're now dynamically switching team based on the URL!

Enabling the team switcher

We're doing well, but the team switcher that you normally have in the top right of the page no longer lists your teams - it only lets you create a new one. Let's fix that now.

Edit app/Http/Controllers/HomeController.php again and remove the lines we added earlier so that the home view loads fine. If we click the icon in the top right it doesn't show our teams. This section of the UI is controlled by resources/views/vendor/spark/nav/teams.blade.php. Edit that file now.

I'm sure there's a way to do this with Vue, but I don't know enough to accomplish that, so I fell back to PHP. Delete everything after the comment <!-- Switch Current Team --> and replace it with the following:

php
@if (isset($currentUser))
@foreach ($currentUser->teams as $t)
@php $currentParams['currentTeam'] = $t->slug; @endphp
<li>
<a href="{{ route($currentRoute, $currentParams) }}">
@if ($currentUser->current_team_id == $t->id)
<span>
<i class="fa fa-fw fa-btn fa-check text-success"></i>{{ $t->name }}
</span>
@else
<span>
<img src="{{ $t->photo_url }}" class="spark-team-photo-xs"><i class="fa fa-btn"></i>{{ $t->name }}
</span>
@endif
</a>
</li>
@endforeach
@endif

Finally, there are three variables that we need to make available to every view, $currentUser, $currentRoute and $currentParams. The first is so that we can iterate over their list of teams and output them in the menu, and the second is so that we can make the link go to the same page but for a different team. The final variable is so that URL generation is handled automatically if there are any additional parameters available in the URL. The easiest way to ensure that these are available is to add some more middleware:

bash
touch src/Middleware/ProvideUserAndRoute.php

Edit that file and add the following contents:

php
<?php
namespace mheap\Middleware;
use Auth;
use Closure;
use View;
class ProvideUserAndRoute {
public function handle($request, Closure $next)
{
$route = $request->route();
View::share('currentUser', Auth::user());
View::share('currentRoute', $route->getName());
View::share('currentParams', $route->parameters());
return $next($request);
}
}

Register this middleware in app/Http/Kernel.php and add the following to $routeMiddleware:

php
'provideUserRoute' => \mheap\Middleware\ProvideUserAndRoute::class,

Then update your route in routes/web.php to add provideUserRoute to the list of required middlewares:

php
Route::group(['prefix' => '{team_slug}', 'middleware' => ['auth', 'fetchTeam', 'provideUserRoute']], function(){
Route::get('/home', 'HomeController@show')->name('home');
});

At this point, you can visit your application again and take a look at the menu in the top right. You should see all of your teams listed and be able to click on them to go to the current page, but on a different team.

The end

I learned all of this through reading various docs, lots of Google searches and a little bit of my own invention. I'm new to Laravel and this probably isn't the best way to go about it but it works for me. If you've got a better solution, please do let me know!