stevendesu
stevendesu

Reputation: 16791

Creating a new ServiceProvider / Facade as a package in Laravel 5

Introduction

I've never worked with a framework before (Zend, CakePHP, etc) and finally decided to sit down and learn one. I'm starting with Laravel because the code looks pretty and unlike some other frameworks I tried to install, the "Hello, World!" example worked on the first try.

The Goal

For the time being, I want my app to do something very simple:

  1. User submits a request in the form of: GET /dist/lat,lng

  2. The app uses the remote IP address and MaxMind to determine $latitude1 and $longitude1

  3. This request path is parsed for $latitude2 and $longitude2

  4. Using these two positions, we calculate the distance between them. To do this I'm using Rafael Fragoso's WorldDistance PHP class

Since I plan to re-use this function in later projects, it didn't seem right to throw all of the code into the /app directory. The two reusable parts of the application were:

  1. A service provider that connects to MaxMind and returns a latitude and longitude
  2. A service provider that takes two points on a globe and returns the distance

If I build facades correctly then instead of my routes.php file being a mess of closures within closures, I can simply write:

Route::get('dist/{input}', function($input){
    $input = explode( "," , $input );

    return Distance::getDistance( GeoIP::getLocation(), $input );
});

What I've tried

Initial Attempt

For the first service provider, I found Daniel Stainback's Laravel 5 GeoIP service provider. It didn't install as easily as it should have (I had to manually copy geoip.php to the /config directory, update /config/app.php by hand, and run composer update and php artisan optimize) however it worked: A request to GET /test returned all of my information.

For the second service provider, I started by trying to mimic the directory structure and file naming convention of the GeoIP service provider. I figured that if I had the same naming convention, the autoloader would be able to locate my class. So I created /vendor/stevendesu/worlddistance/src/Stevendesu/WorldDistance\WorldDistanceServiceProvider.php:

<?php namespace Stevendesu\WorldDistance;

use Illuminate\Support\ServiceProvider;

class WorldDistanceServiceProvider extends ServiceProvider {

    protected $defer = false;

    public function register()
    {
        // Register providers.
        $this->app['distance'] = $this->app->share(function($app)
        {
            return new WorldDistance();
        });
    }

    public function provides()
    {
        return ['distance'];
    }

}

I then added this to my /config/app.php:

        'Stevendesu\WorldDistance\WorldDistanceServiceProvider',

This fails with a fatal error:

FatalErrorException in ProviderRepository.php line 150:
Class 'Stevendesu\WorldDistance\WorldDistanceServiceProvider' not found

Using WorkBench

Since this utterly failed I figured that there must be some other file dependency: maybe without composer.json or without a README it gives up. I don't know. So I started to look into package creation. Several Google searches for "create package laravel 5" proved fruitless. Either:

  1. They were using Laravel 4.2, in which case the advice was "run php artisan workbench vendor/package --resources"

Or

  1. They were using Laravel 5, in which case the docs were completely useless

The official Laravel 5 docs give you plenty of sample code, saying things like:

All you need to do is tell Laravel where the views for a given namespace are located. For example, if your package is named "courier", you might add the following to your service provider's boot method:

public function boot()
{
    $this->loadViewsFrom(__DIR__.'/path/to/views', 'courier');
}

This makes the assumption that you have a service provider to put a boot method in

Nothing in the docs says how to create a service provider in such a way that it will actually be loaded by Laravel.

I also found several different resources all of which assume you have a repository and you just want to include it in your app, or assume you have "workbench". Nothing about creating a new package entirely from scratch.

PHP Artisan did not even have a "workbench" command, and there was no "workbench.php" file in /config, so anything I found related to workbench was worthless. I started doing some research on Workbench and found several different questions on StackOverflow.

After a long time and some experimentation, I managed to get laravel/workbench into my composer.json, composer update, composer install, manually build a workbench.php config file, and finally use the PHP Artisan Workbench command to make a new package:

php artisan workbench Stevendesu/WorldDistance --resources

This created a directory: /workbench/stevendesu/world-distance with a number of sub-directories and only one file: /workbench/stevendesu/world-distance/src/Stevendesu/WorldDistance/WorldDistanceServiceProvider.php

This service provider class looked essentially identical to the file I created before, except that it was in the /workbench directory instead of the /vendor directory. I tried reloading the page and I still got the fatal error:

FatalErrorException in ProviderRepository.php line 150:
Class 'Stevendesu\WorldDistance\WorldDistanceServiceProvider' not found

I also tried php artisan vendor:publish. I don't really know what this command does and the description wasn't helpful, so maybe it would help? It didn't.

Question

How do I create a new service provider as a package so that in future projects I can simply include this package and have all the same functionality? Or rather, what did I do wrong so that the package I created isn't working?

Upvotes: 4

Views: 12116

Answers (1)

stevendesu
stevendesu

Reputation: 16791

After two days of playing with this I managed to find the solution. I had assumed that the directory structure mapped directly to the autoloader's path that it checked (e.g. attempting to access a class Stevendesu\WorldDistance\WorldDistanceServiceProvider would look in vendor/stevendesu/world-distance/WorldDistanceServiceProvider)... This isn't the case.

Reading through the composer source code to see how it actually loads the files, it builds a "classmap" - essentially a gigantic array mapping classes to their respective files. This file is built when you run composer update or composer install - and it will only be built correctly if composer knows the details of your package. That is - if your package is included in your project's composer.json file

I created a local git repository outside of my app then added my package to my app's composer.json file then ran composer update -- suddenly everything worked perfectly.

As for the:

It didn't install as easily as it should have

the secret sauce here was first add the service provider to /config/app.php then, second run php artisan vendor:publish

Upvotes: 5

Related Questions