Package Design 101
Marcel Pociot

Managing partner and developer at Beyondcode

Why should you care? Concentrate on your code features, reuse logic throughout multiple projects, documentation for "free"

When you're working at a company that builds online shops, adding a CMS wouldn't be a core feature (doesn't directly help folks sell things online). In another package, though, the CMS is its own core domain and you don't have to care about the online shop around it.

Once you do this and build multiple projects, you can reuse those components and how you want to use them. Boss comes in and requests another CMS on another store, you've already got that CMS built out.

Let's build a new package!

Badges and badge rules

We're going to do this with a package boilerplate, TDD, package implementation, publishing Can define vendor info, license, etc. License is important; without a license, default is that it's proprietary.

Downloaded boilerplate, we've got composer.json file and others. Since it's laravel-related, added requires for PHP ^7.1 and illuminate/support 5.8.*

Require-dev section has phpunit/phpunit and orchestra/testbench

Autoloading is set up ("Beyondcode\LaravelBadges\": "src"

Since we want to create tests first,

public function it_can_create_badges()
  $badge = Badge::create(['name' => 'one-year-membership', 'description' => 'thing']);
  $this->assert(true, $badge->exists());

Fails because we don't have a Badge class yet.

To make lives easier, create a Badge class

Problem is connection() on null, but PHPUnit doesn't know off the bat that it's in Laravel. Replace PHPUnit test case with Orchestra\Testbench\TestCase.

Re-run test, gets exception that database connection is denied. Go to phpunit.xml file and set up environment variable for DB_CONNECTION to set it to "testing" (does in-memory SQlite).

Re-run, error is that the table doesn't exist. Let's create a migration! It's not namespaced, but class CreateBadgesTable extends \Illuminate\Database\Migrations\Migration, then do the regular table creation stuff. Badge table and Badgeable table (morphale)

Back in the test, do a setUp() function, parent::setup(), then require_once the migration and new up the migration and call up on it (new \CreatBadgesTable())->up()). Tests are green!

But when sharing the package out, others need to know the migration exists. Add a service provider with register/boot methods. In the boot method, we're going to publish our database/migrations/create_badge_table, then move it to the database_path('migrations/' . date(Y-m-d'...)

Now let's do our next test: it_can_associate_badges()

Create a badge, then write code that describes how you'd want to consume it in code

BadgeProvider::grant('one-yet-membership')->to(TestUser::class)->when(function ($user) {
  return $user->create_at->lt(now()->subYear());

User needs a name, email, password, created_at.

$this->assertCount(1, $user->badges);

Also, need to set up a TestUser class and set the table for it to 'users'

Doesn't work. We don't have a BadgeProvider class. Create one, fill it out given what we had described in the test. Fails

In function applyBadges, listen for an 'eloquent.saved: ' $this->model, then in the callback, syncWithoutDetaching. Trying to keep a badge from getting associated with a user twice.

Tests pass, now I want to create a Facade class. BadgeProvider extends Facade, then protected static function getFacadeAccessor returns \Beyondcode\LaravelBadges\BadgeProvider.

Test now needs to know package aliases

See the tests and stuff at

That's it! We built a package and "pushed" it to GitHub and published in Packagist