├── tests ├── Helpers │ ├── Actors.php │ ├── Models.php │ ├── Objects.php │ ├── Targets.php │ ├── migrations.php │ └── factories │ │ └── ModelFactory.php ├── Unit │ ├── FeedTest.php │ └── FacadeTest.php └── TestCase.php ├── src ├── Events │ ├── Event.php │ ├── FeedCreated.php │ ├── FeedDeleted.php │ ├── ActivityCreated.php │ └── ActivityDeleted.php ├── Contracts │ ├── ReturnsExtraData.php │ ├── ActivityActor.php │ ├── ActivityObject.php │ ├── ActivityTarget.php │ └── Followable.php ├── Exceptions │ ├── InvalidActorException.php │ └── InvalidActivityVerbException.php ├── Models │ ├── Follow.php │ ├── FeedActivity.php │ ├── Feed.php │ └── Activity.php ├── ActivityStreamsFacade.php ├── Traits │ ├── HasFeed.php │ └── Followable.php ├── Console │ └── Commands │ │ └── MakeFeedCommand.php ├── Managers │ ├── ConfigurationManager.php │ └── ActivityManager.php ├── ValueObjects │ ├── Actor.php │ ├── Target.php │ ├── ActivityObject.php │ └── Verbs.php ├── ActivityStreamsServiceProvider.php └── ActivityStreams.php ├── config └── activity_streams.php ├── .travis.yml ├── .gitignore ├── phpunit.xml ├── composer.json ├── database └── migrations │ └── create_activity_streams_tables.php └── README.md /tests/Helpers/Actors.php: -------------------------------------------------------------------------------- 1 | [ 6 | // 'FOO' => 'foo', 7 | // 'BAR' => 'bar', 8 | ], 9 | ]; -------------------------------------------------------------------------------- /src/Contracts/ReturnsExtraData.php: -------------------------------------------------------------------------------- 1 | feed = $feed; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Events/FeedDeleted.php: -------------------------------------------------------------------------------- 1 | feed = $feed; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Events/ActivityCreated.php: -------------------------------------------------------------------------------- 1 | activity = $activity; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Events/ActivityDeleted.php: -------------------------------------------------------------------------------- 1 | activity = $activity; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Models/Follow.php: -------------------------------------------------------------------------------- 1 | morphedByMany(Feed::class, 'followable'); 17 | } 18 | } -------------------------------------------------------------------------------- /src/ActivityStreamsFacade.php: -------------------------------------------------------------------------------- 1 | belongsTo(Feed::class, 'feed_id'); 17 | } 18 | 19 | public function activity() 20 | { 21 | return $this->belongsTo(Activity::class, 'activity_id'); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/Helpers/Models.php: -------------------------------------------------------------------------------- 1 | 'This is a test', 17 | ]; 18 | } 19 | } 20 | 21 | class Blog extends Model {} -------------------------------------------------------------------------------- /tests/Helpers/Objects.php: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/Unit 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/Unit/FeedTest.php: -------------------------------------------------------------------------------- 1 | create(); 14 | 15 | $data = [ 16 | 'title' => 'My Feed', 17 | 'description' => 'My description' 18 | ]; 19 | 20 | $feed = $user->createFeed($data); 21 | 22 | $this->assertEquals($data, $feed->extra); 23 | 24 | $this->assertDatabaseHas(TestCase::FEEDS_TABLE, [ 25 | 'id' => 1, 26 | 'feedable_type' => get_class($user), 27 | 'feedable_id' => $user->id 28 | ]); 29 | } 30 | } -------------------------------------------------------------------------------- /tests/Helpers/migrations.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('name'); 19 | $table->string('email')->unique(); 20 | $table->string('password'); 21 | $table->rememberToken(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('users'); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Traits/HasFeed.php: -------------------------------------------------------------------------------- 1 | morphTo(); 20 | } 21 | 22 | public function feed(): MorphOne 23 | { 24 | return $this->morphOne(Feed::class, 'feedable'); 25 | } 26 | 27 | /** 28 | * Create a feed. 29 | * 30 | * @param array $extra 31 | * @return false|Model 32 | */ 33 | public function createFeed(array $extra = []) 34 | { 35 | return $this->feed()->save(new Feed(['extra' => $extra])); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Console/Commands/MakeFeedCommand.php: -------------------------------------------------------------------------------- 1 | argument('class'); 20 | $this->id = $this->argument('id'); 21 | 22 | try { 23 | app($class); 24 | 25 | $feed = new Feed([ 26 | 'feedable_type' => $class, 27 | 'feedable_id' => $this->id, 28 | ]); 29 | 30 | $feed->save(); 31 | 32 | dd($feed->toArray()); 33 | } catch (Exception $exception) { 34 | $this->error($exception->getMessage()); 35 | } 36 | 37 | } 38 | } -------------------------------------------------------------------------------- /src/Models/Feed.php: -------------------------------------------------------------------------------- 1 | 'array', 21 | ]; 22 | 23 | public static function boot() 24 | { 25 | parent::boot(); 26 | 27 | static::created(function($feed) { 28 | event(new FeedCreated($feed)); 29 | }); 30 | 31 | static::deleted(function($feed) { 32 | event(new FeedDeleted($feed)); 33 | }); 34 | } 35 | 36 | public function activities() 37 | { 38 | return $this->belongsToMany( 39 | Activity::class, 40 | 'feed_activities', 41 | 'feed_id', 42 | 'activity_id' 43 | ); 44 | } 45 | } -------------------------------------------------------------------------------- /src/Managers/ConfigurationManager.php: -------------------------------------------------------------------------------- 1 | configuration = config('activity_streams'); 21 | } 22 | 23 | /** 24 | * @param string $verb 25 | * @throws InvalidActivityVerbException 26 | */ 27 | public function validateVerb(string $verb) 28 | { 29 | if (!in_array($verb, $this->getVerbs())) { 30 | throw new InvalidActivityVerbException(sprintf('Invalid verb provided: %s', $verb)); 31 | } 32 | } 33 | 34 | public function getVerbs() 35 | { 36 | $verbsDefinitions = new ReflectionClass(Verbs::class); 37 | $verbs = array_merge($verbsDefinitions->getConstants(), $this->configuration['verbs']); 38 | 39 | return $verbs; 40 | } 41 | } -------------------------------------------------------------------------------- /src/Models/Activity.php: -------------------------------------------------------------------------------- 1 | 'array', 25 | 'target_data' => 'array', 26 | 'object_data' => 'array', 27 | ]; 28 | 29 | public static function boot() 30 | { 31 | parent::boot(); 32 | 33 | static::created(function ($activity) { 34 | event(new ActivityCreated($activity)); 35 | }); 36 | 37 | static::deleted(function ($activity) { 38 | event(new ActivityDeleted($activity)); 39 | }); 40 | } 41 | 42 | public function feeds() 43 | { 44 | return $this->hasMany(FeedActivity::class); 45 | } 46 | } -------------------------------------------------------------------------------- /src/Traits/Followable.php: -------------------------------------------------------------------------------- 1 | $this->getKey(), 14 | 'follower_type' => get_class($followable), 15 | ]); 16 | 17 | return $followable->follows()->save($follow); 18 | } 19 | 20 | public function unfollow(Model $followable) 21 | { 22 | $followable->follows() 23 | ->where('follower_id', $this->getKey()) 24 | ->where('follower_type', get_class($followable)) 25 | ->delete(); 26 | } 27 | 28 | public function isFollowed(Model $followable) 29 | { 30 | return !!$this->follows() 31 | ->where('follower_id', $followable->getKey()) 32 | ->where('follower_type', get_class($followable)) 33 | ->count(); 34 | } 35 | 36 | public function getFollowersCountAttribute() 37 | { 38 | return $this->follows()->count(); 39 | } 40 | 41 | public function follows() 42 | { 43 | return $this->morphMany(Follow::class, 'followable'); 44 | } 45 | 46 | public function followers() 47 | { 48 | return $this->morphTo(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/ValueObjects/Actor.php: -------------------------------------------------------------------------------- 1 | actorType = $actorType; 27 | $this->actorIdentifier = $actorIdentifier; 28 | $this->extraData = $extraData; 29 | } 30 | 31 | public static function createFromModel(Model $model, $extraData = []): Actor 32 | { 33 | if ($model instanceof ReturnsExtraData) { 34 | $extraData = $model->getExtraData(); 35 | } 36 | 37 | return new static(get_class($model), $model->getKey(), $extraData); 38 | } 39 | 40 | public function getType(): string 41 | { 42 | return $this->actorType; 43 | } 44 | 45 | public function getIdentifier(): string 46 | { 47 | return $this->actorIdentifier; 48 | } 49 | 50 | public function getExtraData(): array 51 | { 52 | return $this->extraData; 53 | } 54 | } -------------------------------------------------------------------------------- /src/ValueObjects/Target.php: -------------------------------------------------------------------------------- 1 | targetType = $targetType; 29 | $this->targetIdentifier = $targetIdentifier; 30 | $this->extraData = $extraData; 31 | } 32 | 33 | public static function createFromModel(Model $model, $extraData = []): Target 34 | { 35 | if ($model instanceof ReturnsExtraData) { 36 | $extraData = $model->getExtraData(); 37 | } 38 | 39 | return new static(get_class($model), $model->getKey(), $extraData); 40 | } 41 | 42 | public function getType(): string 43 | { 44 | return $this->targetType; 45 | } 46 | 47 | public function getIdentifier(): string 48 | { 49 | return $this->targetIdentifier; 50 | } 51 | 52 | public function getExtraData(): array 53 | { 54 | return $this->extraData; 55 | } 56 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "musonza/laravel-activity-streams", 3 | "description": "Laravel Package to help with feeds and activity streams in your application", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "tmusonza", 9 | "email": "tinashemusonza@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.1", 14 | "laravel/framework": "5.6.*|5.7.*|5.8.*|^6.0|^7.0" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^7.0|^8.0", 18 | "orchestra/testbench": "^3.6|^3.6|^3.8|^4.0", 19 | "orchestra/database": "^3.6|^3.6|^3.8|^4.0", 20 | "mockery/mockery": "^1.0.0" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Musonza\\ActivityStreams\\": "src/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Musonza\\ActivityStreams\\Tests\\": "tests" 30 | }, 31 | "files": [ 32 | "tests/Helpers/Models.php", 33 | "tests/Helpers/Targets.php", 34 | "tests/Helpers/Objects.php" 35 | ] 36 | }, 37 | "scripts": { 38 | "test": "phpunit" 39 | }, 40 | "extra": { 41 | "laravel": { 42 | "providers": [ 43 | "Musonza\\ActivityStreams\\ActivityStreamsServiceProvider" 44 | ], 45 | "aliases": { 46 | "ActivityStreams": "Musonza\\ActivityStreams\\ActivityStreamsFacade" 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/ValueObjects/ActivityObject.php: -------------------------------------------------------------------------------- 1 | objectType = $objectType; 35 | $this->objectIdentifier = $objectIdentifier; 36 | $this->extraData = $extraData; 37 | } 38 | 39 | public static function createFromModel(Model $model, $extraData = []): ActivityObject 40 | { 41 | if ($model instanceof ReturnsExtraData) { 42 | $extraData = $model->getExtraData(); 43 | } 44 | 45 | return new static(get_class($model), $model->getKey(), $extraData); 46 | } 47 | 48 | public function getType(): string 49 | { 50 | return $this->objectType; 51 | } 52 | 53 | public function getIdentifier(): string 54 | { 55 | return $this->objectIdentifier; 56 | } 57 | 58 | public function getExtraData(): array 59 | { 60 | return $this->extraData; 61 | } 62 | } -------------------------------------------------------------------------------- /tests/Helpers/factories/ModelFactory.php: -------------------------------------------------------------------------------- 1 | define(User::class, function (Faker $faker) { 22 | static $password; 23 | 24 | return [ 25 | 'name' => $faker->name, 26 | 'email' => $faker->unique()->safeEmail, 27 | 'password' => $password ?: $password = bcrypt('secret'), 28 | 'remember_token' => 'vdvNDHDHHDXY798e9', 29 | ]; 30 | }); 31 | 32 | $factory->define(Activity::class, function (Faker $faker) { 33 | $actor = factory(User::class)->create(); 34 | $activityObject = new SampleObject(); 35 | $target = new SampleTarget(); 36 | 37 | return [ 38 | 'verb' => Verbs::VERB_PURCHASE, 39 | 'actor_type' => get_class($actor), 40 | 'actor_id' => $actor->getKey(), 41 | 'actor_data' => $actor->toArray(), 42 | 'object_type' => $activityObject->getType(), 43 | 'object_id' => $activityObject->getIdentifier(), 44 | 'object_data' => $activityObject->getExtraData(), 45 | 'target_type' => $target->getType(), 46 | 'target_id' => $target->getIdentifier(), 47 | 'target_data' => $target->getExtraData(), 48 | ]; 49 | }); -------------------------------------------------------------------------------- /src/ActivityStreamsServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishMigrations(); 25 | $this->publishConfig(); 26 | } 27 | 28 | public function register(): void 29 | { 30 | $this->registerBinds(); 31 | $this->registerCommands(); 32 | } 33 | 34 | /** 35 | * Publish package's migrations. 36 | * 37 | * @return void 38 | */ 39 | public function publishMigrations() 40 | { 41 | $timestamp = date('Y_m_d_His', time()); 42 | $stub = __DIR__.'/../database/migrations/create_activity_streams_tables.php'; 43 | $target = $this->app->databasePath() . '/migrations/' . $timestamp . '_create_activity_streams_tables.php'; 44 | $this->publishes([$stub => $target], 'activity.streams.migrations'); 45 | } 46 | 47 | /** 48 | * Publish package's config file. 49 | * 50 | * @return void 51 | */ 52 | public function publishConfig() 53 | { 54 | $this->publishes([ 55 | __DIR__.'/../config' => config_path(), 56 | ], 'activity.streams.config'); 57 | } 58 | 59 | private function registerBinds(): void 60 | { 61 | $this->app->bind('activity_streams', function () { 62 | return $this->app->make(ActivityStreams::class); 63 | }); 64 | } 65 | 66 | private function registerCommands(): void 67 | { 68 | if ($this->app->runningInConsole()) { 69 | $this->commands([ 70 | MakeFeedCommand::class 71 | ]); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | artisan('migrate', ['--database' => 'testbench']); 25 | $this->withFactories(__DIR__ . '/Helpers/factories'); 26 | 27 | (new CreateActivityStreamsTables())->up(); 28 | (new CreateTestTables())->up(); 29 | } 30 | 31 | /** 32 | * Define environment setup. 33 | * 34 | * @param Application $app 35 | * 36 | * @return void 37 | */ 38 | protected function getEnvironmentSetUp($app) 39 | { 40 | parent::getEnvironmentSetUp($app); 41 | 42 | // Setup default database to use sqlite :memory: 43 | $app['config']->set('database.default', 'testbench'); 44 | $app['config']->set('database.connections.testbench', [ 45 | 'driver' => 'sqlite', 46 | 'database' => ':memory:', 47 | 'prefix' => '', 48 | ]); 49 | 50 | $app['config']->set('activity_streams', include(__DIR__ . '/../config/activity_streams.php')); 51 | } 52 | 53 | protected function getPackageProviders($app) 54 | { 55 | return [ 56 | ConsoleServiceProvider::class, 57 | ActivityStreamsServiceProvider::class, 58 | ]; 59 | } 60 | 61 | protected function getPackageAliases($app) 62 | { 63 | return [ 64 | 'ActivityStreams' => ActivityStreamsFacade::class, 65 | ]; 66 | } 67 | 68 | public function tearDown(): void 69 | { 70 | (new CreateActivityStreamsTables())->down(); 71 | (new CreateTestTables())->down(); 72 | parent::tearDown(); 73 | } 74 | } -------------------------------------------------------------------------------- /database/migrations/create_activity_streams_tables.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('feedable_type'); 19 | $table->bigInteger('feedable_id')->unsigned(); 20 | $table->unique(['feedable_id', 'feedable_type']); 21 | $table->text('extra')->nullable(); 22 | $table->timestamps(); 23 | }); 24 | 25 | Schema::create('activities', function (Blueprint $table) { 26 | $table->bigIncrements('id'); 27 | $table->string('actor_type'); 28 | $table->string('actor_id'); 29 | $table->text('actor_data')->nullable(); 30 | $table->string('verb'); 31 | $table->string('object_type'); 32 | $table->string('object_id'); 33 | $table->text('object_data')->nullable(); 34 | $table->string('target_type')->nullable(); 35 | $table->string('target_id')->nullable(); 36 | $table->text('target_data')->nullable(); 37 | $table->timestamps(); 38 | }); 39 | 40 | Schema::create('feed_activities', function (Blueprint $table) { 41 | $table->bigIncrements('id'); 42 | $table->bigInteger('activity_id')->unsigned(); 43 | $table->bigInteger('feed_id')->unsigned(); 44 | $table->unique(['feed_id', 'activity_id']); 45 | $table->text('extra')->nullable(); 46 | $table->timestamps(); 47 | 48 | $table->foreign('activity_id') 49 | ->references('id') 50 | ->on('activities') 51 | ->onDelete('cascade'); 52 | 53 | $table->foreign('feed_id') 54 | ->references('id') 55 | ->on('feeds') 56 | ->onDelete('cascade'); 57 | 58 | }); 59 | 60 | Schema::create('follows', function (Blueprint $table) { 61 | $table->bigIncrements('id'); 62 | $table->string('follower_id'); 63 | $table->string('follower_type'); 64 | $table->string('followable_id'); 65 | $table->string('followable_type'); 66 | $table->timestamps(); 67 | }); 68 | } 69 | 70 | /** 71 | * Reverse the migrations. 72 | * 73 | * @return void 74 | */ 75 | public function down() 76 | { 77 | Schema::dropIfExists('follows'); 78 | Schema::dropIfExists('feed_activities'); 79 | Schema::dropIfExists('activities'); 80 | Schema::dropIfExists('feeds'); 81 | } 82 | } -------------------------------------------------------------------------------- /src/ValueObjects/Verbs.php: -------------------------------------------------------------------------------- 1 | activity = $activity; 55 | $this->configurationManager = $configurationManager; 56 | } 57 | 58 | /** 59 | * @param ActivityActor $actor 60 | * @return $this 61 | */ 62 | public function setActor(ActivityActor $actor): self 63 | { 64 | $this->actor = $actor; 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * @param string $verb 71 | * @return ActivityManager 72 | * @throws InvalidActivityVerbException 73 | */ 74 | public function setVerb(string $verb): self 75 | { 76 | $this->configurationManager->validateVerb($verb); 77 | 78 | $this->verb = $verb; 79 | 80 | return $this; 81 | } 82 | 83 | public function targetModel(Model $model) 84 | { 85 | return $this->setTarget(Target::createFromModel($model)); 86 | } 87 | 88 | public function setTarget(ActivityTarget $target): self 89 | { 90 | $this->target = $target; 91 | 92 | return $this; 93 | } 94 | 95 | public function setObject(ActivityObject $activityObject): self 96 | { 97 | $this->activityObject = $activityObject; 98 | 99 | return $this; 100 | } 101 | 102 | public function createActivity(): Activity 103 | { 104 | $activityData = [ 105 | 'actor_type' => $this->actor->getType(), 106 | 'actor_id' => $this->actor->getIdentifier(), 107 | 'actor_data' => $this->actor->getExtraData(), 108 | 'verb' => $this->verb, 109 | 'object_type' => $this->activityObject->getType(), 110 | 'object_id' => $this->activityObject->getIdentifier(), 111 | 'object_data' => $this->activityObject->getExtraData(), 112 | 'target_type' => $this->target->getType(), 113 | 'target_id' => $this->target->getIdentifier(), 114 | 'target_data' => $this->target->getExtraData(), 115 | ]; 116 | 117 | $activity = $this->activity->newInstance($activityData); 118 | 119 | $activity->save(); 120 | 121 | return $activity; 122 | } 123 | 124 | public function addActivityToFeed(Feed $feed, Activity $activity): void 125 | { 126 | $feed->activities()->attach($activity); 127 | } 128 | 129 | public function addMultipleActivitiesToFeed(Feed $feed, Collection $filteredActivities) 130 | { 131 | $feed->activities()->insert($filteredActivities->toArray()[0]); 132 | } 133 | } -------------------------------------------------------------------------------- /src/ActivityStreams.php: -------------------------------------------------------------------------------- 1 | activityManager = $activityManager; 35 | $this->configurationManager = $configurationManager; 36 | } 37 | 38 | /** 39 | * Add an activity to a feed. 40 | * 41 | * @param Feed $feed 42 | * @param $activity 43 | */ 44 | public function addActivityToFeed(Feed $feed, $activity): void 45 | { 46 | if ($activity instanceof Collection) { 47 | $filteredActivities = $activity->whereInstanceOf(Activity::class); 48 | 49 | // TODO batch insert 50 | foreach ($filteredActivities as $activity) { 51 | $this->activityManager->addActivityToFeed($feed, $activity); 52 | } 53 | // $this->activityManager->addMultipleActivitiesToFeed($feed, $filteredActivities); 54 | return; 55 | } 56 | 57 | $this->activityManager->addActivityToFeed($feed, $activity); 58 | } 59 | 60 | public function addActivityToMultipleFeeds(Collection $feeds, Activity $activity) 61 | { 62 | $filteredFeeds = $feeds->whereInstanceOf(Feed::class); 63 | 64 | foreach ($filteredFeeds as $feed) { 65 | $this->activityManager->addActivityToFeed($feed, $activity); 66 | } 67 | } 68 | 69 | /** 70 | * Sets activity actor. 71 | * 72 | * @param $actor 73 | * @return ActivityStreams 74 | */ 75 | public function setActor($actor): self 76 | { 77 | if ($actor instanceof Model) { 78 | $actor = $this->actorFromModel($actor); 79 | } 80 | 81 | $this->activityManager->setActor($actor); 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * @param Model $model 88 | * @return Actor 89 | */ 90 | public function actorFromModel(Model $model) 91 | { 92 | return Actor::createFromModel($model); 93 | } 94 | 95 | /** 96 | * @param string $verb 97 | * @return $this 98 | * @throws Exceptions\InvalidActivityVerbException 99 | */ 100 | public function setVerb(string $verb) 101 | { 102 | $this->activityManager->setVerb($verb); 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * Set activity target. 109 | * 110 | * @param $target 111 | * @return ActivityStreams 112 | */ 113 | public function setTarget($target): self 114 | { 115 | if ($target instanceof Model) { 116 | $target = $this->targetFromModel($target); 117 | } 118 | 119 | $this->activityManager->setTarget($target); 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * @param Model $model 126 | * @return Target 127 | */ 128 | public function targetFromModel(Model $model) 129 | { 130 | return Target::createFromModel($model); 131 | } 132 | 133 | /** 134 | * Sets activity object. 135 | * 136 | * @param $activityObject 137 | * @return ActivityStreams 138 | */ 139 | public function setObject($activityObject): self 140 | { 141 | if ($activityObject instanceof Model) { 142 | $activityObject = $this->objectFromModel($activityObject); 143 | } 144 | 145 | $this->activityManager->setObject($activityObject); 146 | 147 | return $this; 148 | } 149 | 150 | /** 151 | * @param Model $model 152 | * @return ActivityObject 153 | */ 154 | public function objectFromModel(Model $model) 155 | { 156 | return ActivityObject::createFromModel($model); 157 | } 158 | 159 | /** 160 | * Gets supported verbs. 161 | * 162 | * @return array 163 | */ 164 | public function verbs(): array 165 | { 166 | return $this->configurationManager->getVerbs(); 167 | } 168 | 169 | /** 170 | * Persist the activity. 171 | * 172 | * @return Activity 173 | */ 174 | public function createActivity(): Activity 175 | { 176 | return $this->activityManager->createActivity(); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/musonza/laravel-activity-streams.svg?branch=master)](https://travis-ci.org/musonza/laravel-activity-streams) 2 | 3 | 4 | # Laravel Activity Streams 5 | 6 | 7 | ## Table of Contents 8 | 9 |
Click to expand

10 | 11 | - [Introduction](#introduction) 12 | - [Installation](#installation) 13 | - [Usage](#usage) 14 | - [Facade](#facade) 15 | - [Giving a model an ability to have a Feed](#Giving-a-model-an-ability-to-have-a-Feed) 16 | - [Create a model Feed](#create-a-model-feed) 17 | - [On-Demand Feeds](#On-Demand-Feeds) 18 | - [Create an Activity](#create-an-activity) 19 | - [Actors](#actors) 20 | - [Valid Actors](#valid-actors) 21 | - [Targets](#targets) 22 | - [Valid Targets](#valid-targets) 23 | - [Objects](#objects) 24 | - [Valid Objects](#valid-objects) 25 | - [Get supported verbs](#Get-supported-verbs) 26 | - [Add an activity to a Feed](#Add-an-activity-to-a-Feed) 27 | - [Add multiple activities to a Feed](#Add-multiple-activities-to-a-Feed) 28 | - [Events](#events) 29 | - [Configuration](#configuration) 30 | - [FAQ](#faq) 31 | 32 |

33 | 34 | ## Introduction 35 | 36 | This package enables you to have activity streams in your laravel applications. 37 | 38 | ## Installation 39 | 40 | Install with composer 41 | 42 | ```sh 43 | composer require musonza/laravel-activity-streams 44 | ``` 45 | 46 | Once the composer installation is finished, you can add alias for the facade. Open `config/app.php`, and make the following update: 47 | 48 | 1) Add a new item to the `aliases` array: 49 | 50 | ```php 51 | 'ActivityStreams' => Musonza\ActivityStreams\ActivityStreamsFacade::class, 52 | ``` 53 | 54 | 1) Publish the configuration file into your app's `config` directory, by running the following command: 55 | 56 | ``` 57 | php artisan vendor:publish --tag="activity.streams.config" 58 | ``` 59 | 60 | 1) Publish the migrations into your app's `migrations` directory, by running the following command: 61 | 62 | ``` 63 | php artisan vendor:publish --tag="activity.streams.migrations" 64 | ``` 65 | 66 | 1) Run the migrations: 67 | 68 | ``` 69 | php artisan migrate 70 | ``` 71 | 72 | ## Usage 73 | 74 | #### Facade 75 | 76 | Whenever you use the `ActivityStreams` facade in your code, remember to add the following line to your namespace imports: 77 | 78 | ```php 79 | use ActivityStreams; 80 | ``` 81 | 82 | #### Giving a model an ability to have a Feed 83 | 84 | Use the `HasFeed` trait to allow a model to have a feed. 85 | ```php 86 | createFeed(); 102 | ``` 103 | 104 | #### On-Demand Feeds 105 | 106 | Sometimes you may want to create a Feed that's does not belong to a Model. For example, 107 | you want to add activities to a Trending Feed for your application: 108 | 109 | Create a class to represent the Trending feed under a namespace of choice 110 | 111 | ```php 112 | unique(['some-unique-id', 'App\Trending']);` 129 | 130 | #### Create an Activity 131 | 132 | An example of an activity will be something like **John liked a photo in 2018Album** 133 | 134 | | | | 135 | | ------------- |:-------------:| 136 | | Actor |John | 137 | | Verb | like | 138 | | Object | photo | 139 | | Target | 2018Album | 140 | 141 | 142 | ```php 143 | use ActivityStreams; 144 | use Musonza\ActivityStreams\ValueObjects\Verbs; 145 | 146 | $activity = ActivityStreams::setActor($actor) 147 | ->setVerb(Verbs::VERB_LIKE) 148 | ->setObject($object) 149 | ->setTarget($target) 150 | ->createActivity(); 151 | ``` 152 | 153 | #### Actors 154 | 155 | ##### Valid Actors 156 | 157 | You can pass in an Eloquent Model as an actor or any Object that implements `Musonza\ActivityStreams\Contracts\ActivityActor` interface 158 | 159 | #### Targets 160 | 161 | ##### Valid Targets 162 | 163 | You can pass in an Eloquent Model as a target or any Object that implements `Musonza\ActivityStreams\Contracts\ActivityTarget` interface 164 | 165 | #### Objects 166 | 167 | ##### Valid Objects 168 | 169 | You can pass in an Eloquent Model as an object or any Object that implements `Musonza\ActivityStreams\Contracts\ActivityObject` interface 170 | 171 | 172 | #### Get supported verbs 173 | ```php 174 | $verbs = ActivityStreams::verbs(); 175 | ``` 176 | 177 | #### Add an activity to a Feed 178 | ```php 179 | ActivityStreams::addActivityToFeed($feed, $activity); 180 | ``` 181 | 182 | #### Add multiple activities to a Feed 183 | Adds a `Collection` of activities to a `Feed` 184 | 185 | ```php 186 | ActivityStreams::addActivityToFeed($feed, $activities); 187 | ``` 188 | 189 | #### Add an activity to multiple Feeds 190 | Adds an `Activity` to a `Collection` feeds 191 | 192 | ```php 193 | ActivityStreams::addActivityToMultipleFeeds($feeds, $activity); 194 | ``` 195 | 196 | ## Events 197 | 198 | You can leverage and listen for the following events to perform actions in 199 | your application. For instance you can listen for an `ActivityCreated` event and depending on 200 | your business logic add the created event to a `Feed` or multiple feeds. 201 | 202 | #### ActivityCreated 203 | `Musonza\ActivityStreams\Models\Activity\ActivityCreated` 204 | 205 | #### ActivityDeleted 206 | `Musonza\ActivityStreams\Models\Activity\ActivityDeleted` 207 | 208 | #### FeedCreated 209 | `Musonza\ActivityStreams\Models\Activity\FeedCreated` 210 | 211 | #### FeedDeleted 212 | `Musonza\ActivityStreams\Models\Activity\FeedDeleted` 213 | 214 | ## Configuration 215 | 216 | 217 | ## FAQ 218 | See more on Activity Streams specifications [here](http://activitystrea.ms/) 219 | -------------------------------------------------------------------------------- /tests/Unit/FacadeTest.php: -------------------------------------------------------------------------------- 1 | activityStreams = app(ActivityStreams::class); 28 | } 29 | 30 | public function testGetDefinedVerbs() 31 | { 32 | $verbs = $this->activityStreams->verbs(); 33 | 34 | $this->assertIsArray($verbs); 35 | $this->assertEquals('post', $verbs['VERB_POST']); 36 | } 37 | 38 | /** 39 | * @dataProvider activitiesDataProvider 40 | * @throws InvalidActivityVerbException 41 | */ 42 | public function testCreateActivity($actor, $target, $object) 43 | { 44 | $actor = isset($actor['is_model']) ? factory(User::class)->create() : $actor['value']; 45 | $target = isset($target['is_model']) ? factory(User::class)->create() : $target['value']; 46 | $object = isset($object['is_model']) ? factory(User::class)->create() : $object['value']; 47 | 48 | $activity = $this->activityStreams->setActor($actor) 49 | ->setVerb(Verbs::VERB_POST) 50 | ->setTarget($target) 51 | ->setObject($object) 52 | ->createActivity(); 53 | 54 | $this->assertInstanceOf(Activity::class, $activity); 55 | } 56 | 57 | public function testAddActivityToFeed() 58 | { 59 | $user = factory(User::class)->create(); 60 | $feed = $user->createFeed(); 61 | 62 | $activity = $this->activityStreams->setActor($user) 63 | ->setVerb(Verbs::VERB_POST) 64 | ->setObject(new SampleObject()) 65 | ->setTarget(new SampleTarget()) 66 | ->createActivity(); 67 | 68 | $this->activityStreams->addActivityToFeed($feed, $activity); 69 | 70 | $this->assertEquals(1, $feed->activities()->count()); 71 | } 72 | 73 | public function testAddMultipleActivitiesToFeed() 74 | { 75 | $user = factory(User::class)->create(); 76 | 77 | /** @var Feed $feed */ 78 | $feed = $user->createFeed(); 79 | 80 | $activities = factory(Activity::class, 2)->create(); 81 | 82 | $this->activityStreams->addActivityToFeed($feed, $activities); 83 | 84 | $this->assertEquals(2, $feed->activities()->count()); 85 | } 86 | 87 | public function testAddActivityToMultipleFeeds() 88 | { 89 | $users = factory(User::class, 5)->create(); 90 | 91 | $feeds = []; 92 | foreach ($users as $user) { 93 | $feeds[] = $user->createFeed(); 94 | } 95 | 96 | $feeds = collect($feeds); 97 | 98 | /** @var Activity $activity */ 99 | $activity = factory(Activity::class)->create(); 100 | 101 | $this->activityStreams->addActivityToMultipleFeeds($feeds, $activity); 102 | 103 | $this->assertEquals(5, $activity->feeds()->count()); 104 | } 105 | 106 | public function testFeedFollowing() 107 | { 108 | /** @var User $user1 */ 109 | $user1 = factory(User::class)->create(); 110 | /** @var Feed $user1Feed */ 111 | $user1Feed = $user1->createFeed(); 112 | 113 | /** @var User $user1 */ 114 | $user2 = factory(User::class)->create(); 115 | /** @var Feed $user2Feed */ 116 | $user2Feed = $user2->createFeed(); 117 | 118 | $user1Feed->follow($user2Feed); 119 | 120 | $this->assertDatabaseHas('follows',[ 121 | 'follower_id' => $user1Feed->getKey(), 122 | 'follower_type' => get_class($user1Feed), 123 | 'followable_type' => get_class($user2Feed), 124 | 'followable_id' => $user2Feed->getKey(), 125 | ]); 126 | 127 | } 128 | 129 | public function activitiesDataProvider() 130 | { 131 | return [ 132 | [ 133 | 'actor' => [ 134 | 'is_model' => true, 135 | 'value' => null, 136 | ], 137 | 'target' => [ 138 | 'is_model' => true, 139 | 'value' => null, 140 | ], 141 | 'object' => [ 142 | 'is_model' => true, 143 | 'value' => null, 144 | ], 145 | ], 146 | [ 147 | 'actor' => [ 148 | 'value' => new Actor('twitter_user', 126626) 149 | ], 150 | 'target' => [ 151 | 'value' => new SampleTarget() 152 | ], 153 | 'object' => [ 154 | 'is_model' => true, 155 | 'value' => null, 156 | ], 157 | ], 158 | [ 159 | 'actor' => [ 160 | 'is_model' => true, 161 | 'value' => null, 162 | ], 163 | 'target' => [ 164 | 'value' => new SampleTarget() 165 | ], 166 | 'object' => [ 167 | 'value' => new SampleObject() 168 | ], 169 | ], 170 | [ 171 | 'actor' => [ 172 | 'value' => new Actor('twitter_user', 126626) 173 | ], 174 | 'target' => [ 175 | 'value' => new SampleTarget() 176 | ], 177 | 'object' => [ 178 | 'value' => new SampleObject() 179 | ], 180 | ], 181 | [ 182 | 'actor' => [ 183 | 'value' => new Actor('twitter_user', 126626) 184 | ], 185 | 'target' => [ 186 | 'value' => new SampleTarget() 187 | ], 188 | 'object' => [ 189 | 'is_model' => true, 190 | 'value' => null, 191 | ], 192 | ], 193 | ]; 194 | } 195 | } --------------------------------------------------------------------------------