Upgrade to Pro — share decks privately, control downloads, hide ads and more …

In-depth Laravel Tricks and Tips

In-depth Laravel Tricks and Tips

My presentation for Laracon EU 2014 in Amsterdam.

Andreas Lutro

August 28, 2014
Tweet

Other Decks in Programming

Transcript

  1. Laracon EU 2014 Applications as packages & in-depth Laravel tips

    and tricks - GitHub: anlutro Twitter: @AndreasLutro
  2. In this presentation Laravel apps as packages - creating re-usable

    modules Best practices learned and gotchas on the way Squeezing the most out of Laravel Striking a balance between configurability and practicality Git, Composer, Packagist, Satis Assumes you know DI, facades. Some best practices have been avoided, implementations left out or very crude to save space. Every tutorial is wrong
  3. Why and when? You want to create a common ground

    for new projects, whether it's custom CMSes or something else You want to be able to do feature updates across multiple apps without copy-pasting code Why and when not? Each of your projects have too specific needs to split codebases into multiple repositories Makes more sense for a traditional HTML application than an API- centric application Maybe as API-centric server-side dev libraries catch up, more of this advice will be relevant You hate doing composer update
  4. Making a package $ ed app/config/workbench.php # fill in personal

    info $ php artisan workbench laracon/demo-pkg --resources $ cd workench/laracon/demo-pkg $ tree -aI 'vendor|.gitkeep' ├── composer.json ├── composer.lock ├── .gitignore ├── phpunit.xml ├── public ├── src │ ├── config │ ├── controllers │ ├── lang │ ├── Laracon │ │ └── DemoPkg │ │ └── DemoPkgServiceProvider.php │ ├── migrations │ └── views ├── tests └── .travis.yml
  5. Workbench pitfalls Use all lowercase and regular dashes for vendor/package

    Stick to the default package directory structure Application has access to workbench classes, not vice-versa Don't rely on global namespace aliases If your package requires the entire laravel/framework package you will run into problems In the long run, aim to create packages without workbenches
  6. Planning "Core" package for common ground Widget system, dynamic menu

    system, more? Authentication and authorization (login and permissions) Do you really need controllers and views? Plan the public API methods/classes carefully - you'll hate yourself for them later on
  7. Service providers Your package's app/start/global.php IoC bindings, middleware, routes, event

    listeners, view composers, route filters, configuration, error handlers... IoC bindings in register(), everything else in boot()
  8. IoC container public function register() { $this->app->singleton('Laracon\DemoPkg\SignupManager'); $this->app->bindShared('Laracon\DemoPkg\PdfMaker', function($app) {

    return new PdfMaker(/* ... */); }); // allows $app['pdf'], App::make('pdf') $this->app->alias('Laracon\DemoPkg\PdfMaker', 'pdf'); } bind = factory -- bindShared = singleton Never resolve stuff from the IoC in register!
  9. IoC container public function register() { // bad - invoked

    immediately $this->app['foo'] = new MyClass($this->app['bar']); // good - invoked when requested, deferred $this->app->bindShared('foo', function($app) { return new MyClass($app['bar']); }); }
  10. IoC container // "core" service provider $this->app->singleton('Laracon\DemoPkg\UserManager'); // lazily set

    other services via extend $this->app->extend('Laracon\DemoPkg\UserManager', function($userManager, $app) { $service = $app->make('Laracon\OtherPkg\SomeService'); $userManager->setOtherService($service); return $userManager; }); extend calls may sometimes have to be done in boot(), not register()
  11. public function boot() { $this->package('laracon/demo-pkg'); if ($prefix = $this->app['config']->get('demo-pkg::route-prefix')) {

    $this->app['router']->group(['prefix' => $prefix], function() { $this->registerRoutes(); }); } else { $this->registerRoutes(); } } protected function registerRoutes() { require __DIR__.'/routes.php'; } Anything you can put in app/routes.php or app/start/global.php you can put in boot Configurable route prefix - quick and dirty
  12. Gotcha public function boot() { $this->app['url']->action('MyController@myMethod'); } public function boot()

    { $this->app->booted(function() { $this->app['url']->action('MyController@myMethod'); }); } register » boot » global.php, routes.php » booted booted callback means every other service provider ++ has been booted
  13. Database interaction Most similar to regular app development Using models

    in fine, but allow extension - use DI to make them swappable Extending and modifying concrete classes tends to break things. Write interfaces for models/repositories to be more flexible Include migrations, models/repositories, but be prepared for the package user modifying them
  14. Routes Always use controllers, always namespace Override controllers by extending,

    App::bind Route names as an abstraction Make URLs configurable to prevent conflicts Reading routes from config - github.com/anlutro/laravel-4-core, search for RouteProviderTrait
  15. Templates / views Let your core package define a set

    of CSS/JS frameworks/libraries - Difficult to have many CSS/JS dependencies without conflicts If you really need customizable classes/markup, consider writing dynamic widget/menu/form builder systems Strip the view of all logic, delegate to controllers and view composers Create your own shared utility classes in combination vith view composers
  16. Static sidebar use Illuminate\View\View as ViewObject; View::creator('sidebar', function(ViewObject $view) {

    $view->items = []; }); View::composer('sidebar', function(ViewObject $view) { $view->items[] = new SidebarItem($title, $content); }, $priority); @foreach ($items as $item) <div class="sidebar-item"> <h1 class="sidebar-item-title">{{ $item->title }}</h1> <div class="sidebar-item-content">{{ $item->content }}</div> </div> @endforeach $content can be a View::make()! Can be extended to admin dashboards, widget areas... Write abstract composer/creator classes for more advanced logic - protected helper methods
  17. class SidebarCreator { public function compose($view) { $view->sidebar = new

    Collection; } } abstract class AbstractScriptComposer { final function compose($view) { $view->sidebar->add($this->getSidebarItems()); } protected abstract function getBodyScripts(); }
  18. class ScriptCollection { public function add($id, $url) { $this->scripts[$id] =

    $url; } public function remove($id) { unset($this->scripts[$id]); } public function render() { $str = ''; foreach ($this->items as $item) $str .= '<script src="'.$item.'"></script>'; return $str; } }
  19. Menu class MenuManager { public function getMenu($name) { return $this->menus[$name];

    } public function createMenu($name) { return $this->menus[$name] = new MenuCollection; } } class MenuCollection { public function addItem($url, $title) { $this->items[] = new MenuItem($url, $title); } public function addSubmenu($title) { $this->items[] = new SubmenuItem($title, new MenuCollection); } } github.com/KnpLabs/KnpMenu
  20. return View::make('demo-pkg::path.to.view'); @extends('hardcoded.layout') @append('sidebar-menu') <li><a href="/path/to/something">Link to something</a></li> @stop @section('content')

    @if (Auth::user()->isAdmin()) You rule! @endif Content here. @stop @section('scripts') <script type="text/javascript">alert('!');</script> @stop
  21. protected function getLayout() { // can also rely on View::alias

    instead of config return View::make( Config::get('demo-pkg::layout-view') ); } return $this->getLayout() ->nest('content', 'demo-pkg::path.to.view', [ 'isAdmin' => Auth::user()->isAdmin(), ]); class MyComposer { public function compose($view) { Menu::getMenu('sidebar') ->addItem( URL::action('MyController@myAction'), 'Link to something' ); $view->bodyScripts->append( URL::asset('demo-pkg::path/to/script.js') ); } } View::composer('demo-pkg::path.to.*', 'MyComposer'); @if ($isAdmin) You rule! @endif Content here.
  22. Enough of these components and you have a CMS. Plugin

    and hook functionality comes almost for free in Laravel. Use and abuse it!
  23. Testing - methods Automated tests and decent test coverage is

    a must Extend regular PHPUnit_Framework_TestCase for unit tests To test controllers/other framework-dependant stuff: Everything in a service provider = your package is as easy to test as a regular app github.com/anlutro/laravel-testing
  24. Testing - amounts Unit tests for small classes with few/none

    dependencies Controller tests with a seeded test database - thorough and easy Use phpunit's --coverage-html to make sure you're not leaving critical parts of your code untested
  25. Distribution Composer, packagist, github/bitbucket Git - tags to mark stable

    releases. Use semver Packagist runs cron jobs, can take up to 10 minutes for a new version to register Disadvantage: Packages have to be publicly accessible
  26. Satis Self-hosted mini-version of packagist Mirror packages for when/if packagist

    goes down, host your own non- public packages Fastest setup: Set up web server with restrictions, put all git repos in webroot, run git update-server-info as a post-update hook Restrict per IP and/or use HTTP basic (composer can be configured to auto-auth using HTTP basic)
  27. Satis workflow Make changes to package, git commit, git tag

    git push triggest post-update hook Satis rebuilds index, package is available for download composer update my-vendor/my-package in applications
  28. Conclusion Build a "core" package that other packages can plug

    into Learn best practices, avoid some pitfalls/conveniences Plan public classes/methods, use semver, save yourself a headache Write widget/dashboard/sidebar/menu manager classes, add them to view object with creators, manipulate them in composers Unit/functional tests over browser testing