diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9082713
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.DS_Store
+composer.lock
+/vendor/
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..dba7bb9
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,18 @@
+language: php
+
+php:
+ - 5.5
+ - 5.6
+ - 7.0
+
+before_script:
+ - travis_retry composer self-update
+ - travis_retry composer install --prefer-source --no-interaction
+
+script:
+ - vendor/bin/phpunit --coverage-clover=coverage.xml
+
+before_install:
+ - pip install --user codecov
+after_success:
+ - codecov
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6144e2c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,152 @@
+# Versionable
+## Easy to use Model versioning for Laravel 4 and Laravel 5
+
+
+
+
+[](https://codecov.io/github/mpociot/versionable?branch=master)
+[](https://scrutinizer-ci.com/g/mpociot/versionable/?branch=master)
+[](https://travis-ci.org/mpociot/versionable)
+
+Keep track of all your model changes and revert to previous versions of it.
+
+
+```php
+// Restore to the previous change
+$content->previousVersion()->revert();
+
+// Get model from a version
+$oldModel = Version::find(100)->getModel();
+```
+
+
+## Contents
+
+- [Installation](#installation)
+- [Implementation](#implementation)
+- [Usage](#usage)
+ - [Exclude attributes from versioning](#exclude)
+ - [Retrieving all versions associated to a model](#retrieve)
+ - [Getting a diff of two versions](#diff)
+ - [Revert to a previous version](#revert)
+- [FAQ](#faq)
+- [License](#license)
+
+
+## Installation
+
+In order to add Versionable to your project, just add
+
+ "mpociot/versionable": "~2.0"
+
+to your composer.json. Then run `composer install` or `composer update`.
+
+Or run `composer require mpociot/versionable ` if you prefere that.
+
+Run the migrations to create the "versions" table that will hold all version information.
+
+```bash
+php artisan migrate --path=vendor/mpociot/versionable/src/migrations
+```
+
+
+## Usage
+
+Let the Models you want to set under version control use the `VersionableTrait`.
+
+```php
+class Content extends Model {
+
+ use Mpociot\Versionable\VersionableTrait;
+
+}
+```
+That's it!
+
+Every time you update your model, a new version containing the previous attributes will be stores in your database.
+
+All timestamps and the possible soft-delete timestamp will be ignored.
+
+
+### Exclude attributes from versioning
+
+Sometimes you don't want to create a version *every* time an attribute on your model changes. For example your User model might have a `last_login_at` attribute.
+I'm pretty sure you don't want to create a new version of your User model every time that user logs in.
+
+To exclude specific attributes from versioning, add a new array property to your model named `dontVersionFields`.
+
+```php
+class User extends Model {
+
+ use Mpociot\Versionable\VersionableTrait;
+
+ /**
+ * @var array
+ */
+ protected $dontVersionFields = [ 'last_login_at' ];
+
+}
+```
+
+
+### Retrieving all versions associated to a model
+
+To retrieve all stored versions use the `versions` attribute on your model.
+
+This attribute can also be accessed like any other Laravel relation, since it is a `MorphMany` relation.
+
+```php
+$model->versions;
+```
+
+
+### Getting a diff of two versions
+
+If you want to know, what exactly has changed between two versions, use the version model's `diff` method.
+
+The diff method takes a version model as an argument. This defines the version to diff against. If no version is provided, it will use the current version.
+
+```php
+/**
+ * Create a diff against the current version
+ */
+$diff = $page->previousVersion()->diff();
+
+
+/**
+ * Create a diff against a specific version
+ */
+$diff = $page->currentVersion()->diff( $version );
+```
+
+The result will be an associative array containing the attribute name as the key, and the different attribute value.
+
+
+### Revert to a previous version
+
+Saving versions is pretty cool, but the real benefit will be the ability to revert to a specific version.
+
+There are multiple ways to do this.
+
+**Revert to the previous version**
+
+You can easiliy revert to the version prior to the currently active version using:
+
+```php
+$content->previousVersion()->revert();
+```
+
+**Revert to a specific version ID**
+
+You can also revert to a specific version ID of a model using:
+
+```php
+$revertedModel = Version::find( $version_id )->revert();
+```
+
+
+
+
+## License
+
+Versionable is free software distributed under the terms of the MIT license.
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..dd20272
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,37 @@
+{
+ "name": "mpociot/captainhook",
+ "license": "MIT",
+ "description": "Add webhooks to your Laravel app.",
+ "keywords": ["webhooks", "laravel", "hook", "events"],
+ "homepage": "http://github.com/mpociot/captainhook",
+ "authors": [
+ {
+ "name": "Marcel Pociot",
+ "email": "m.pociot@gmail.com"
+ }
+ ],
+ "support": {
+ "issues": "https://github.com/mpociot/captainhook/issues",
+ "source": "https://github.com/mpociot/captainhook"
+ },
+ "require": {
+ "php": ">=5.3.0",
+ "illuminate/support": "~5.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "4.7.*",
+ "mockery/mockery": "dev-master",
+ "illuminate/database": "~5.0",
+ "illuminate/events": "~5.0",
+ "orchestra/testbench": "~3.0",
+ "guzzlehttp/guzzle": ">=4.0"
+ },
+ "autoload": {
+ "psr-0": {
+ "Mpociot\\CaptainHook": "src/"
+ }
+ },
+ "scripts": {
+ "test": "phpunit"
+ }
+}
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..bf98672
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ tests/
+
+
+
+
+ src/Mpociot/
+
+
+
diff --git a/src/Mpociot/CaptainHook/CaptainHookServiceProvider.php b/src/Mpociot/CaptainHook/CaptainHookServiceProvider.php
new file mode 100644
index 0000000..09daa2d
--- /dev/null
+++ b/src/Mpociot/CaptainHook/CaptainHookServiceProvider.php
@@ -0,0 +1,208 @@
+client = new Client();
+ $this->cache = app('Illuminate\Contracts\Cache\Repository');
+ $this->publishMigration();
+ }
+
+ /**
+ * Register the service provider.
+ *
+ * @return void
+ */
+ public function register()
+ {
+ $this->registerEventListeners();
+ $this->registerCommands();
+ }
+
+ /**
+ * Publish migration
+ */
+ protected function publishMigration()
+ {
+ $published_migration = glob(database_path('/migrations/*_captain_hook_setup_table.php'));
+ if (count($published_migration) === 0) {
+ $this->publishes([
+ __DIR__ . '/../../database/2015_10_29_000000_captain_hook_setup_table.php' => database_path('/migrations/' . date('Y_m_d_His') . '_captain_hook_setup_table.php'),
+ ], 'migrations');
+ }
+ }
+
+ /**
+ * Register all active event listeners
+ */
+ protected function registerEventListeners()
+ {
+ foreach ($this->listeners as $eventName) {
+ $this->app[ "events" ]->listen($eventName, [$this, "handleEvent"]);
+ }
+ }
+
+ /**
+ * @param array $listeners
+ */
+ public function setListeners($listeners)
+ {
+ $this->listeners = $listeners;
+
+ $this->registerEventListeners();
+ }
+
+ /**
+ * @param array $webhooks
+ */
+ public function setWebhooks($webhooks)
+ {
+ $this->webhooks = $webhooks;
+ $this->getCache()->rememberForever(Webhook::CACHE_KEY, function () {
+ return $this->webhooks;
+ });
+ }
+
+ /**
+ * @return \Illuminate\Support\Collection
+ */
+ public function getWebhooks()
+ {
+ if (!$this->getCache()->has(Webhook::CACHE_KEY)) {
+ $this->getCache()->rememberForever(Webhook::CACHE_KEY, function () {
+ return Webhook::all();
+ });
+ }
+ return collect($this->getCache()->get(Webhook::CACHE_KEY));
+ }
+
+ /**
+ * @return \Illuminate\Contracts\Cache\Repository
+ */
+ public function getCache()
+ {
+ return $this->cache;
+ }
+
+ /**
+ * @param \Illuminate\Contracts\Cache\Repository $cache
+ */
+ public function setCache($cache)
+ {
+ $this->cache = $cache;
+ }
+
+ /**
+ * @param ClientInterface $client
+ */
+ public function setClient($client)
+ {
+ $this->client = $client;
+ }
+
+ /**
+ * Event listener.
+ * @param $eventData
+ */
+ public function handleEvent($eventData)
+ {
+ $eventName = Event::firing();
+ $webhooks = $this->getWebhooks();
+
+ $this->callWebhooks($webhooks->where("event", $eventName), $eventData);
+ }
+
+ /**
+ * Call all webhooks asynchronous
+ *
+ * @param array $webhooks
+ * @param $eventData
+ */
+ private function callWebhooks($webhooks, $eventData)
+ {
+ foreach ($webhooks as $webhook) {
+ $this->client->postAsync($webhook[ "url" ], [
+ "body" => json_encode($this->createRequestBody($eventData)),
+ "verify" => false
+ ]);
+ }
+ }
+
+ /**
+ * Create the request body for the event data.
+ * Override this method if necessary to post different data.
+ *
+ * @param $eventData
+ *
+ * @return array
+ */
+ protected function createRequestBody($eventData)
+ {
+ return $eventData;
+ }
+
+ /**
+ * Register the artisan commands
+ */
+ protected function registerCommands()
+ {
+ $this->app[ 'hook.list' ] = $this->app->share(function ($app) {
+ return new Commands\ListWebhooks();
+ });
+
+ $this->app[ 'hook.add' ] = $this->app->share(function ($app) {
+ return new Commands\AddWebhook();
+ });
+
+ $this->app[ 'hook.delete' ] = $this->app->share(function ($app) {
+ return new Commands\DeleteWebhook();
+ });
+
+ $this->commands(
+ 'hook.list',
+ 'hook.add',
+ 'hook.delete'
+ );
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/Mpociot/CaptainHook/Commands/AddWebhook.php b/src/Mpociot/CaptainHook/Commands/AddWebhook.php
new file mode 100644
index 0000000..485082c
--- /dev/null
+++ b/src/Mpociot/CaptainHook/Commands/AddWebhook.php
@@ -0,0 +1,55 @@
+url = $this->argument("url");
+ $hook->event = $this->argument("event");
+ try {
+ $hook->save();
+ $this->info("The webhook was saved successfully.");
+ $this->info("Event: " . $hook->event);
+ $this->info("URL: " . $hook->url);
+ } catch (Exception $e) {
+ $this->error("The webhook couldn't be added to the database " . $e->getMessage());
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/src/Mpociot/CaptainHook/Commands/DeleteWebhook.php b/src/Mpociot/CaptainHook/Commands/DeleteWebhook.php
new file mode 100644
index 0000000..98e3adc
--- /dev/null
+++ b/src/Mpociot/CaptainHook/Commands/DeleteWebhook.php
@@ -0,0 +1,48 @@
+argument("id");
+ $hook = Webhook::find($id);
+ if ($hook === null) {
+ $this->error("Webhook with ID " . $id . " could not be found.");
+ } else {
+ $hook->delete();
+ $this->info("The webhook was deleted successfully.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Mpociot/CaptainHook/Commands/ListWebhooks.php b/src/Mpociot/CaptainHook/Commands/ListWebhooks.php
new file mode 100644
index 0000000..fa006a4
--- /dev/null
+++ b/src/Mpociot/CaptainHook/Commands/ListWebhooks.php
@@ -0,0 +1,41 @@
+get();
+ $this->table(['id', 'url', 'event'], $all->toArray());
+ }
+}
\ No newline at end of file
diff --git a/src/Mpociot/CaptainHook/Webhook.php b/src/Mpociot/CaptainHook/Webhook.php
new file mode 100644
index 0000000..c3663cb
--- /dev/null
+++ b/src/Mpociot/CaptainHook/Webhook.php
@@ -0,0 +1,43 @@
+increments('id');
+ $table->string('url');
+ $table->string('event');
+ $table->timestamps();
+ });
+
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+
+ Schema::drop("webhooks");
+
+ }
+}
\ No newline at end of file
diff --git a/tests/CaptainHookTest.php b/tests/CaptainHookTest.php
new file mode 100644
index 0000000..82287a7
--- /dev/null
+++ b/tests/CaptainHookTest.php
@@ -0,0 +1,187 @@
+artisan('migrate', [
+ '--database' => 'testing',
+ '--realpath' => realpath(__DIR__.'/../src/database'),
+ ]);
+ }
+
+ public function tearDown()
+ {
+ \Cache::forget( Webhook::CACHE_KEY );
+ }
+
+ /**
+ * Define environment setup.
+ *
+ * @param \Illuminate\Foundation\Application $app
+ * @return void
+ */
+ protected function getEnvironmentSetUp($app)
+ {
+ // Setup default database to use sqlite :memory:
+ $app['config']->set('database.default', 'testing');
+
+ \Schema::create('test_models', function ($table) {
+ $table->increments('id');
+ $table->string('name');
+ $table->timestamps();
+ $table->softDeletes();
+ });
+ }
+
+ public function testEloquentEventListenerGetCalled()
+ {
+ $provider = $this->app->getProvider("Mpociot\\CaptainHook\\CaptainHookServiceProvider");
+ $provider->setWebhooks([
+ [
+ "event" => "eloquent.saved: TestModel",
+ "url" => "http://foo.baz/hook"
+ ],
+ [
+ "event" => "eloquent.saved: TestModel",
+ "url" => "http://foo.bar/hook"
+ ],
+ [
+ "event" => "eloquent.deleted: TestModel",
+ "url" => "http://foo.baz/foo"
+ ]
+ ]);
+
+
+
+ $client = m::mock("GuzzleHttp\\Client");
+
+ $client->shouldReceive("postAsync")
+ ->twice();
+
+ $client->shouldReceive("postAsync")
+ ->with( "http://foo.baz/hook", m::any() );
+
+ $client->shouldReceive("postAsync")
+ ->with( "http://foo.bar/hook", m::any() );
+
+ $provider->setClient( $client );
+
+ // Trigger eloquent event
+ $obj = new TestModel();
+ $obj->name = "Test";
+ $obj->save();
+ }
+
+ public function testCustomEventListener()
+ {
+ $provider = $this->app->getProvider("Mpociot\\CaptainHook\\CaptainHookServiceProvider");
+ $provider->setListeners([
+ "TestEvent"
+ ]);
+ $provider->setWebhooks([
+ [
+ "event" => "TestEvent",
+ "url" => "http://foo.bar/hook"
+ ]
+ ]);
+
+ $model = new TestModel();
+ $model->name = "Test";
+
+ $client = m::mock("GuzzleHttp\\Client");
+
+ $client->shouldReceive("postAsync")
+ ->once()
+ ->with( "http://foo.bar/hook", ['body' => json_encode(["testModel" => $model]), 'verify' => false] );
+
+ $provider->setClient( $client );
+
+ // Trigger eloquent event
+ \Event::fire( new TestEvent( $model ) );
+ }
+
+ public function testUsesWebhooksFromCache()
+ {
+ $webhook = new Webhook();
+ $webhook->url = "http://test.foo/saved";
+ $webhook->event = "eloquent.saved: TestModel";
+ $webhook->save();
+
+ $webhook = new Webhook();
+ $webhook->url = "http://test.foo/deleted";
+ $webhook->event = "eloquent.deleted: TestModel";
+ $webhook->save();
+
+ $provider = $this->app->getProvider("Mpociot\\CaptainHook\\CaptainHookServiceProvider");
+ $this->assertCount( 2, $provider->getWebhooks() );
+
+ $this->assertTrue( Cache::has( Webhook::CACHE_KEY ) );
+ $this->assertCount( 2, Cache::get( Webhook::CACHE_KEY ) );
+
+ }
+
+ public function testUsesWebhooksFromDatabase()
+ {
+ $webhook = new Webhook();
+ $webhook->url = "http://test.foo/saved";
+ $webhook->event = "eloquent.saved: TestModel";
+ $webhook->save();
+
+ $webhook = new Webhook();
+ $webhook->url = "http://test.bar/saved";
+ $webhook->event = "eloquent.saved: TestModel";
+ $webhook->save();
+
+ $webhook = new Webhook();
+ $webhook->url = "http://test.foo/deleted";
+ $webhook->event = "eloquent.deleted: TestModel";
+ $webhook->save();
+
+
+ $client = m::mock("GuzzleHttp\\Client");
+
+ $client->shouldReceive("postAsync")
+ ->twice();
+
+ $client->shouldReceive("postAsync")
+ ->with( "http://test.foo/saved", m::any() );
+
+ $client->shouldReceive("postAsync")
+ ->with( "http://test.bar/saved", m::any() );
+
+ $provider = $this->app->getProvider("Mpociot\\CaptainHook\\CaptainHookServiceProvider");
+ $provider->setClient( $client );
+
+ $obj = new TestModel();
+ $obj->name = "Test";
+ $obj->save();
+
+ }
+}
+
+class TestModel extends \Illuminate\Database\Eloquent\Model
+{
+
+}
+
+class TestEvent extends \Illuminate\Support\Facades\Event
+{
+ use SerializesModels;
+
+ public function __construct(TestModel $model)
+ {
+ $this->testModel = $model;
+ }
+}
\ No newline at end of file
diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php
new file mode 100644
index 0000000..ba5ac9d
--- /dev/null
+++ b/tests/CommandsTest.php
@@ -0,0 +1,109 @@
+artisan('migrate', [
+ '--database' => 'testing',
+ '--realpath' => realpath(__DIR__.'/../src/database'),
+ ]);
+ }
+
+ public function testCannotAddWebhookWithoutName()
+ {
+ $cmd = m::mock("\\Mpociot\\CaptainHook\\Commands\\AddWebhook[argument,error]");
+
+ $cmd->shouldReceive("error")
+ ->once()
+ ->with( m::type("string") );
+
+ $cmd->shouldReceive("argument")
+ ->twice();
+
+ $cmd->shouldReceive("argument")
+ ->with("url")
+ ->andReturn("http://foo.bar");
+
+ $cmd->handle();
+
+ $this->assertTrue(true);
+ }
+
+ public function testCanAddWebhook()
+ {
+ $cmd = m::mock("\\Mpociot\\CaptainHook\\Commands\\AddWebhook[argument,info]");
+
+ $cmd->shouldReceive("argument")
+ ->with("url")
+ ->andReturn("http://foo.bar");
+
+ $cmd->shouldReceive("argument")
+ ->with("event")
+ ->andReturn("TestModelTestModel");
+
+ $cmd->shouldReceive("info")
+ ->with( m::type("string") );
+
+ $cmd->handle();
+
+
+ $this->seeInDatabase("webhooks",[
+ "event" => "TestModelTestModel",
+ "url" => "http://foo.bar",
+ ]);
+ }
+
+ public function testCannotDeleteWebhookWithWrongID()
+ {
+ $webhook = \Mpociot\CaptainHook\Webhook::create([
+ "url" => "http://foo.baz",
+ "event" => "DeleteWebhook",
+ ]);
+ $cmd = m::mock("\\Mpociot\\CaptainHook\\Commands\\DeleteWebhook[argument,error]");
+
+ $cmd->shouldReceive("argument")
+ ->with("id")
+ ->andReturn( null );
+
+ $cmd->shouldReceive("error")
+ ->with( m::type("string") );
+
+ $cmd->handle();
+
+
+ $this->seeInDatabase("webhooks",[
+ "url" => "http://foo.baz",
+ "event" => "DeleteWebhook",
+ ]);
+ }
+
+ public function testCanDeleteWebhook()
+ {
+ $webhook = \Mpociot\CaptainHook\Webhook::create([
+ "url" => "http://foo.baz",
+ "event" => "DeleteWebhook",
+ ]);
+ $cmd = m::mock("\\Mpociot\\CaptainHook\\Commands\\DeleteWebhook[argument,info]");
+
+ $cmd->shouldReceive("argument")
+ ->with("id")
+ ->andReturn( $webhook->getKey() );
+
+ $cmd->shouldReceive("info")
+ ->with( m::type("string") );
+
+ $cmd->handle();
+
+
+ $this->notSeeInDatabase("webhooks",[
+ "url" => "http://foo.baz",
+ "event" => "DeleteWebhook",
+ ]);
+ }
+}
\ No newline at end of file