├── .gitignore ├── LICENSE ├── composer.json ├── migrations └── 2014_09_17_001147_ParleyTables.php ├── nitpick.json ├── phpunit.xml ├── readme.md ├── src ├── Contracts │ └── ParleyableInterface.php ├── Events │ ├── ParleyEvent.php │ ├── ParleyMessageAdded.php │ └── ParleyThreadCreated.php ├── Exceptions │ ├── InvalidMessageFormatException.php │ ├── NonMemberObjectException.php │ ├── NonParleyableMemberException.php │ └── NonReferableObjectException.php ├── Facades │ └── Parley.php ├── Models │ ├── Message.php │ └── Thread.php ├── ParleyManager.php ├── ParleyServiceProvider.php ├── Support │ └── ParleySelector.php └── Traits │ └── ParleyHelpersTrait.php └── tests ├── ParleyManagerTests.php ├── ParleyMessageTests.php ├── ParleyTestCase.php ├── ParleyThreadTests.php ├── TestingEnvironmentTests.php └── _data ├── Group.php ├── User.php ├── Widget.php ├── db ├── .gitignore └── staging.sqlite └── migrations └── 2014_09_18_200617_TestDataTables.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | .idea 6 | .phpunit.result.cache 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Stage Right Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "srlabs/parley", 3 | "description": "Polymorphic Messaging for Laravel", 4 | "keywords": [ 5 | "laravel", 6 | "notifications", 7 | "messaging", 8 | "polymorphic", 9 | "polymorphism" 10 | ], 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Ryan C. Durham", 15 | "email": "ryan@stagerightlabs.com" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.0", 20 | "illuminate/support": "^9.0", 21 | "illuminate/contracts": "^9.0", 22 | "illuminate/database": "^9.0", 23 | "illuminate/events": "^9.0" 24 | }, 25 | "require-dev": { 26 | "orchestra/testbench": "^7.0", 27 | "phpunit/phpunit": "^9.5.10", 28 | "mockery/mockery": "^1.3.1" 29 | }, 30 | "autoload": { 31 | "classmap": [ 32 | "tests" 33 | ], 34 | "psr-4": { 35 | "Parley\\": "src/" 36 | } 37 | }, 38 | "minimum-stability": "stable" 39 | } 40 | -------------------------------------------------------------------------------- /migrations/2014_09_17_001147_ParleyTables.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('subject'); 19 | $table->integer('object_id')->nullable(); 20 | $table->string('object_type')->nullable(); 21 | $table->string('type')->nullable(); 22 | $table->boolean('closed_at')->nullable(); 23 | $table->integer('closed_by_id')->nullable(); 24 | $table->string('closed_by_type')->nullable(); 25 | $table->timestamp('created_at'); 26 | $table->timestamp('updated_at'); 27 | $table->timestamp('deleted_at')->nullable(); 28 | }); 29 | 30 | // Create the 'Parley Messages' table 31 | Schema::create('parley_messages', function($table){ 32 | $table->increments('id'); 33 | $table->text('body'); 34 | $table->string('author_alias')->nullable(); 35 | $table->integer('author_id')->nullable(); 36 | $table->string('author_type')->nullable(); 37 | $table->integer('parley_thread_id'); 38 | $table->timestamp('created_at'); 39 | $table->timestamp('updated_at'); 40 | }); 41 | 42 | // Create the 'Parley Members' table 43 | Schema::create('parley_members', function($table){ 44 | $table->integer('parley_thread_id'); 45 | $table->integer('parleyable_id'); 46 | $table->string('parleyable_type'); 47 | $table->boolean('is_read')->default(0); 48 | $table->boolean('notified')->default(0); 49 | }); 50 | } 51 | 52 | /** 53 | * Reverse the migrations. 54 | * 55 | * @return void 56 | */ 57 | public function down() 58 | { 59 | //Drop the 'Parley Models' table 60 | Schema::drop('parley_threads'); 61 | 62 | // Drop the 'Parley Messages' table 63 | Schema::drop('parley_messages'); 64 | 65 | // Drop the 'Parley Memebers' table 66 | Schema::drop('parley_members'); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /nitpick.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "tests/* 4 | ] 5 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | ./tests/ParleyTestCase.php 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Parley: Polymorphic Messaging for Laravel Applications 2 | 3 | [![Build Status](https://travis-ci.org/SRLabs/Parley.svg?branch=master)](https://travis-ci.org/SRLabs/Parley) 4 | 5 | With Parley you can easily send messages between different object types within a Laravel application. These "conversations" can be bi-directional, allowing for easy communication with your users about topics relevant to your application. 6 | 7 | * Associate threads with reference objects, such as orders or any other eloquent model instance 8 | * Keep track of which members have or haven't "read" the messages 9 | * Optionally mark threads as "open" or "closed" 10 | 11 | Here is an example: 12 | 13 | ```php 14 | Parley::discuss([ 15 | 'subject' => "A New Game has been added", 16 | 'body' => "A new game with {$teamB->name} has been added to the schedule.", 17 | 'alias' => "The Commissioner", 18 | 'author' => $admin, 19 | 'regarding' => $game 20 | ])->withParticipant($teamA); 21 | ``` 22 | 23 | When a player logs in, their unread Parley messages can be retrieved like so: 24 | 25 | ```php 26 | $messages = Parley::gatherFor([$user, $user->team])->unread()->get(); 27 | ``` 28 | 29 | If this user wants to reply to a message, it can be done like this: 30 | 31 | ```php 32 | $thread = Parley::find(1); 33 | $thread->reply([ 34 | 'body' => 'Thanks for the heads up! We will bring snacks.', 35 | 'author' => $user 36 | ]); 37 | ``` 38 | 39 | Additional usage examples and the API can be found [here](http://stagerightlabs.com/projects/parley). 40 | 41 | 42 | ### Installation 43 | 44 | This package can be installed using composer: 45 | 46 | ```shell 47 | $ composer require srlabs/parley 48 | ``` 49 | 50 | Add the Service Provider and Alias to your ```config/app.php``` file: 51 | 52 | ```php 53 | 'providers' => array( 54 | // ... 55 | Parley\ParleyServiceProvider::class, 56 | // ... 57 | ) 58 | ``` 59 | 60 | ```php 61 | 'aliases' => [ 62 | // ... 63 | 'Parley' => Parley\Facades\Parley::class, 64 | // ... 65 | ], 66 | ``` 67 | 68 | Next, publish and run the migrations 69 | 70 | ```shell 71 | php artisan vendor:publish --provider="Parley\ParleyServiceProvider" --tag="migrations" 72 | php artisan migrate 73 | ``` 74 | 75 | Any Eloquent Model that implements the ```Parley\Contracts\ParleyableInterface``` can be used to send or receive Parley messages. To fulfill that contract, you need to have ```getParleyAliasAttribute``` and ```getParleyIdAttribute``` methods available on that model: 76 | 77 | * ```getParleyAliasAttribute()``` - Specify the "display name" for the model participating in a Parley Conversation. For users this could be their username, or their first and last names combined. 78 | * ```getParleyIdAttribute()``` - Specify the integer id you want to have represent this model in the Parley database tables. It is most likely that you will want to use the model's ```id``` attribute here, but that is not always the case. 79 | 80 | NB: While you are required to provide an alias for each Parleyable Model, You are not required to use that alias when creating threads - you can optionally specify a different "alias" attribute when creating messages. 81 | 82 | You are now ready to go! 83 | 84 | ### Events 85 | 86 | Whenever a new thread is created, or a new reply message is added, an event is fired. You can set up your listeners in your EventServiceProvider like so: 87 | 88 | ```php 89 | protected $listen = [ 90 | 'Parley\Events\ParleyThreadCreated' => [ 91 | 'App\Listeners\FirstEventListener', 92 | 'App\Listeners\SecondEventListener' 93 | ], 94 | 'Parley\Events\ParleyMessageAdded' => [ 95 | 'App\Listeners\ThirdEventListener', 96 | 'App\Listeners\FourthEventListener' 97 | ], 98 | ] 99 | ``` 100 | 101 | Each event is passed the Thread object and the author of the current message. You can retrieve these objects using the ```getThread()``` and ```getAuthor``` methods: 102 | 103 | ```php 104 | class AppEventListener 105 | { 106 | 107 | /** 108 | * Handle the event. 109 | * 110 | * @param SiteEvent $event 111 | * @return void 112 | */ 113 | public function handle(ParleyEvent $event) 114 | { 115 | // Fetch the thread 116 | $thread = $event->getThread(); 117 | 118 | // Fetch the author 119 | $author = $event->getAuthor(); 120 | 121 | // ... 122 | } 123 | } 124 | ``` 125 | -------------------------------------------------------------------------------- /src/Contracts/ParleyableInterface.php: -------------------------------------------------------------------------------- 1 | attributes['id'];". 18 | * 19 | * @return int 20 | */ 21 | public function getParleyIdAttribute(); 22 | } 23 | -------------------------------------------------------------------------------- /src/Events/ParleyEvent.php: -------------------------------------------------------------------------------- 1 | thread = $thread; 22 | $this->author = $author; 23 | } 24 | 25 | /** 26 | * @return Thread 27 | */ 28 | public function getThread() 29 | { 30 | return $this->thread; 31 | } 32 | 33 | /** 34 | * @return mixed 35 | */ 36 | public function getAuthor() 37 | { 38 | return $this->author; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Events/ParleyMessageAdded.php: -------------------------------------------------------------------------------- 1 | message = $message; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Exceptions/NonMemberObjectException.php: -------------------------------------------------------------------------------- 1 | message = $message; 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/Exceptions/NonReferableObjectException.php: -------------------------------------------------------------------------------- 1 | belongsTo('Parley\Models\Thread', 'parley_thread_id'); 29 | } 30 | 31 | /** 32 | * Get the Authoring Object of this Message 33 | * 34 | * @return mixed 35 | */ 36 | public function getAuthor() 37 | { 38 | if ($this->author_type == '') { 39 | return null; 40 | } 41 | 42 | return \App::make($this->author_type)->find($this->author_id); 43 | } 44 | 45 | /** 46 | * Set the authoring object for this message 47 | * 48 | * @param string|null $alias 49 | * @param mixed $author 50 | * @return bool 51 | * @throws NonMemberObjectException 52 | * @throws NonReferableObjectException 53 | */ 54 | public function setAuthor($author, $alias = null) 55 | { 56 | // Make sure the author has a valid primary key 57 | $this->confirmObjectIsReferable($author); 58 | 59 | // If an author alias was explicitly specified, use that value instead of the default model alias 60 | $alias = ($alias ? $alias : $author->parley_alias); 61 | 62 | // Associate the new Author with this Message 63 | $this->author_alias = $alias; 64 | $this->author_id = $author->getParleyIdAttribute(); 65 | $this->author_type = get_class($author); 66 | 67 | return $this->save(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Models/Thread.php: -------------------------------------------------------------------------------- 1 | addMember($member); 41 | } 42 | 43 | // Send an alert to any application listeners that might be interested 44 | event(new ParleyThreadCreated($this, $this->getThreadAuthor())); 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * Convenience wrapper for withParticipants() 51 | * 52 | * @return $this 53 | */ 54 | public function withParticipant() 55 | { 56 | $this->withParticipants(func_get_args()); 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * Convenience wrapper for the Add Member method 63 | * 64 | * @return $this 65 | */ 66 | public function addParticipant($member) 67 | { 68 | $this->addMember($member); 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * Add a single member this thread 75 | * 76 | * @param $member 77 | * @return void 78 | * @throws NonParleyableMemberException 79 | */ 80 | protected function addMember($member) 81 | { 82 | // If we have been passed an Eloquent collection, add each member recursively 83 | if ($member instanceof Collection) { 84 | foreach ($member->all() as $m) { 85 | $this->addMember($m); 86 | } 87 | 88 | return; 89 | } 90 | 91 | // Or perhaps we have been given an array... 92 | if (is_array($member)) { 93 | foreach ($member as $m) { 94 | $this->addMember($m); 95 | } 96 | 97 | return; 98 | } 99 | 100 | // Is this Member parleyable? 101 | $this->confirmObjectIsParleyable($member); 102 | 103 | // Add the member to the Parley 104 | \DB::table('parley_members')->insert(array( 105 | 'parley_thread_id' => $this->id, 106 | 'parleyable_id' => $member->getParleyIdAttribute(), 107 | 'parleyable_type' => get_class($member) 108 | )); 109 | 110 | return; 111 | } 112 | 113 | /** 114 | * Remove a Member from this Thread 115 | * 116 | * @param $member 117 | * 118 | * @return void 119 | * @throws NonParleyableMemberException 120 | */ 121 | protected function removeMember($member) 122 | { 123 | // Is this Member parleyable? 124 | $this->confirmObjectIsParleyable($member, true); 125 | 126 | // Remove this member from the Thread 127 | \DB::table('parley_members') 128 | ->where('parley_thread_id', $this->id) 129 | ->where('parleyable_id', $member->getParleyIdAttribute()) 130 | ->where('parleyable_type', get_class($member)) 131 | ->delete(); 132 | 133 | return; 134 | } 135 | 136 | /** 137 | * Convenience wrapper for the remove member method 138 | * 139 | * @param $member 140 | * @return $this 141 | */ 142 | public function removeParticipant($member) 143 | { 144 | $this->removeMember($member); 145 | 146 | return $this; 147 | } 148 | 149 | /** 150 | * Determine if an member is a member of this thread 151 | * 152 | * @param $member 153 | * 154 | * @return bool 155 | */ 156 | public function isMember($member) 157 | { 158 | // Is this Member parleyable? 159 | $this->confirmObjectIsParleyable($member, true); 160 | 161 | return (count( 162 | \DB::table('parley_members') 163 | ->where('parley_thread_id', $this->id) 164 | ->where('parleyable_id', $member->getParleyIdAttribute()) 165 | ->where('parleyable_type', get_class($member)) 166 | ->get() 167 | ) > 0); 168 | } 169 | 170 | /** 171 | * Convenience wrapper for the isMember method 172 | * 173 | * @param $member 174 | * @return bool 175 | */ 176 | public function isParticipant($member) 177 | { 178 | return $this->isMember($member); 179 | } 180 | 181 | /** 182 | * Retrieve all the members associated with this Parley Thread 183 | * 184 | * @param array $options 185 | * @return Collection 186 | */ 187 | public function getMembers(array $options = []) 188 | { 189 | $exclusions = array_key_exists('except', $options) ? $this->ensureArrayable($options['except']) : []; 190 | 191 | $members = \DB::table('parley_members')->where('parley_thread_id', $this->id)->get(); 192 | $filteredMembers = new Collection(); 193 | 194 | foreach ($members as $member) { 195 | $exclude = false; 196 | 197 | foreach ($exclusions as $target) { 198 | if ($member->parleyable_id == $target->getParleyIdAttribute() && $member->parleyable_type == get_class($target)) { 199 | $exclude = true; 200 | } 201 | } 202 | 203 | if (! $exclude) { 204 | $object = \App::make($member->parleyable_type)->find($member->parleyable_id); 205 | $filteredMembers->push($object); 206 | } 207 | } 208 | 209 | return $filteredMembers; 210 | } 211 | 212 | /** 213 | * Associate the initial Message Object with this thread. 214 | * 215 | * @param array $messageData 216 | * 217 | * @return $this 218 | * @throws InvalidMessageFormatException 219 | */ 220 | public function setInitialMessage($messageData = array()) 221 | { 222 | // Create the first Message and add it to this thread 223 | $this->createMessage($messageData); 224 | 225 | // Add the author as a member of this Thread 226 | $this->addMember($messageData['author']); 227 | 228 | // We can assume that the author has read their own message. 229 | $this->markReadForMembers($messageData['author']); 230 | } 231 | 232 | /** 233 | * Add a new Message Object to this thread 234 | * 235 | * @param $messageData 236 | * @return Message 237 | * @throws InvalidMessageFormatException 238 | * @throws NonMemberObjectException 239 | */ 240 | public function reply($messageData) 241 | { 242 | // Make sure the author is in fact a member of this thread 243 | if (!array_key_exists('author', $messageData) || !$this->isMember($messageData['author'])) { 244 | throw new NonMemberObjectException; 245 | } 246 | 247 | // Add this messageData to the thread 248 | $this->createMessage($messageData); 249 | 250 | // A new messageData implies that this thread is now unread for all members 251 | $this->markUnreadForAllMembers(); 252 | 253 | // Except the author of the reply, of course. 254 | $this->markReadForMembers($messageData['author']); 255 | 256 | // Change the thread's 'updated_at' timestamp to be in sync with the new messageData timestamp 257 | $this->touch(); 258 | 259 | // Send an alert to any application listeners that might be interested 260 | \Event::dispatch(new ParleyMessageAdded($this, $this->getThreadAuthor())); 261 | 262 | return $this; 263 | } 264 | 265 | /** 266 | * Return the most recent Message associated with this Thread 267 | * 268 | * @return mixed 269 | */ 270 | public function newestMessage() 271 | { 272 | return Message::where('parley_thread_id', $this->id) 273 | ->orderBy('created_at', 'desc') 274 | ->first(); 275 | } 276 | 277 | /** 278 | * Return the most recent Message associated with this Thread 279 | * 280 | * @return mixed 281 | */ 282 | public function originalMessage() 283 | { 284 | return Message::where('parley_thread_id', $this->id) 285 | ->orderBy('created_at', 'asc') 286 | ->first(); 287 | } 288 | 289 | /** 290 | * Return the Collection of Messages associated with this Thread 291 | * 292 | * @param bool $latestFirst 293 | * @return \Illuminate\Support\Collection 294 | */ 295 | public function messages($latestFirst = true) 296 | { 297 | return $this->hasMany('Parley\Models\Message', 'parley_thread_id')->orderBy('created_at', $latestFirst ? 'desc' : 'asc'); 298 | } 299 | 300 | /** 301 | * Set the Object that this Thread is concerned with, if needed. 302 | * 303 | * @param $object 304 | * 305 | * @return mixed 306 | * @throws NonReferableObjectException 307 | */ 308 | public function setReferenceObject($object) 309 | { 310 | // Ensure that this object has a valid primary key 311 | $this->confirmObjectIsReferable($object); 312 | 313 | // Set the object reference fields 314 | $this->object_id = $object->getKey(); 315 | $this->object_type = get_class($object); 316 | 317 | return $this->save(); 318 | } 319 | 320 | /** 321 | * Return the object this Thread is concerned with, if any 322 | * 323 | * @return mixed 324 | */ 325 | public function getReferenceObject() 326 | { 327 | if ($this->object_type == '') { 328 | return null; 329 | } 330 | 331 | return \App::make($this->object_type)->find($this->object_id); 332 | } 333 | 334 | /** 335 | * Remove the Thread's reference Object 336 | * 337 | * @return bool 338 | */ 339 | public function clearReferenceObject() 340 | { 341 | $this->object_id = null; 342 | $this->object_type = ''; 343 | 344 | return $this->save(); 345 | } 346 | 347 | /** 348 | * Get the authoring object for the first messageData in the thread 349 | * 350 | * @return mixed 351 | */ 352 | public function getThreadAuthor() 353 | { 354 | return $this->originalMessage()->getAuthor(); 355 | } 356 | 357 | /** 358 | * Mark this Thread as Closed 359 | * 360 | * @param $member - Thread Member Object 361 | * @return bool 362 | */ 363 | public function closedBy($member) 364 | { 365 | // Setting a value to the "closed_at" field marks the thread as closed. 366 | $this->closed_at = new Carbon; 367 | 368 | // Make a note of which member closed the thread 369 | $this->closed_by_id = $member->getParleyIdAttribute(); 370 | $this->closed_by_type = get_class($member); 371 | 372 | return $this->save(); 373 | } 374 | 375 | /** 376 | * Has this Thread been closed? 377 | * 378 | * @return bool 379 | */ 380 | public function isClosed() 381 | { 382 | return (bool)$this->closed_at; 383 | } 384 | 385 | /** 386 | * Return the member that closed the thread 387 | * 388 | * @return null 389 | */ 390 | public function getCloser() 391 | { 392 | // First Make sure this thread has been closed. 393 | if (! $this->isClosed()) { 394 | return null; 395 | } 396 | 397 | // Return the Member who closed the Thread 398 | return $object = \App::make($this->closed_by_type)->find($this->closed_by_id); 399 | } 400 | 401 | /** 402 | * Mark thread as open 403 | * 404 | * @return bool 405 | */ 406 | public function reopen() 407 | { 408 | $this->closed_at = null; 409 | $this->closed_by_id = 0; 410 | $this->closed_by_type = ''; 411 | 412 | return $this->save(); 413 | } 414 | 415 | /** 416 | * Determine if a thread has been marked read for a given user 417 | * 418 | * @param $member 419 | * 420 | * @return bool 421 | */ 422 | public function hasBeenReadByMember($member) 423 | { 424 | $status = \DB::table('parley_members') 425 | ->where('parley_thread_id', $this->id) 426 | ->where('parleyable_id', $member->getParleyIdAttribute()) 427 | ->where('parleyable_type', get_class($member)) 428 | ->value('is_read'); 429 | 430 | return (bool) $status; 431 | } 432 | 433 | /** 434 | * Mark the Thread as read for a given member. 435 | * 436 | * @param array $members 437 | * @return bool 438 | * 439 | */ 440 | public function markReadForMembers($members = array()) 441 | { 442 | $members = $this->ensureArrayable($members); 443 | 444 | foreach ($members as $member) { 445 | \DB::table('parley_members') 446 | ->where('parley_thread_id', $this->id) 447 | ->where('parleyable_id', $member->getParleyIdAttribute()) 448 | ->where('parleyable_type', get_class($member)) 449 | ->update(['is_read' => 1]); 450 | } 451 | 452 | return true; 453 | } 454 | 455 | /** 456 | * Mark the Thread as Read for all members 457 | * 458 | * @return bool 459 | */ 460 | public function markReadForAllMembers() 461 | { 462 | \DB::table('parley_members') 463 | ->where('parley_thread_id', $this->id) 464 | ->update(['is_read' => 0]); 465 | 466 | return true; 467 | } 468 | 469 | /** 470 | * Mark the Thread as Unread for a given member 471 | * 472 | * @param $members 473 | * 474 | * @return bool 475 | */ 476 | public function markUnreadForMembers($members) 477 | { 478 | $members = $this->ensureArrayable($members); 479 | 480 | foreach ($members as $member) { 481 | \DB::table('parley_members') 482 | ->where('parley_thread_id', $this->id) 483 | ->where('parleyable_id', $member->getParleyIdAttribute()) 484 | ->where('parleyable_type', get_class($member)) 485 | ->update(['is_read' => 0]); 486 | } 487 | 488 | return true; 489 | } 490 | 491 | /** 492 | * Mark the Thread as Unread for all members 493 | * 494 | * @return bool 495 | */ 496 | public function markUnreadForAllMembers() 497 | { 498 | \DB::table('parley_members') 499 | ->where('parley_thread_id', $this->id) 500 | ->update(['is_read' => 0]); 501 | 502 | return true; 503 | } 504 | 505 | /** 506 | * Determine if a thread member has been notified about this thread 507 | * 508 | * @param $member 509 | * 510 | * @return bool 511 | */ 512 | public function memberHasBeenNotified($member) 513 | { 514 | $status = \DB::table('parley_members') 515 | ->where('parley_thread_id', $this->id) 516 | ->where('parleyable_id', $member->getParleyIdAttribute()) 517 | ->where('parleyable_type', get_class($member)) 518 | ->value('notified'); 519 | 520 | return (bool) $status; 521 | } 522 | 523 | /** 524 | * Set the "notified" flag for a set of members 525 | * 526 | * @param mixed $members 527 | * 528 | * @return bool 529 | */ 530 | public function markNotifiedForMembers($members = []) 531 | { 532 | $members = $this->ensureArrayable($members); 533 | 534 | foreach ($members as $member) { 535 | \DB::table('parley_members') 536 | ->where('parley_thread_id', $this->id) 537 | ->where('parleyable_id', $member->getParleyIdAttribute()) 538 | ->where('parleyable_type', get_class($member)) 539 | ->update(['notified' => 1]); 540 | } 541 | 542 | return true; 543 | } 544 | 545 | /** 546 | * Remove the notified flag for the given members 547 | * 548 | * @param mixed $members 549 | * 550 | * @return bool 551 | */ 552 | public function removeNotifiedFlagForMembers($members = []) 553 | { 554 | $members = $this->ensureArrayable($members); 555 | 556 | foreach ($members as $member) { 557 | \DB::table('parley_members') 558 | ->where('parley_thread_id', $this->id) 559 | ->where('parleyable_id', $member->getParleyIdAttribute()) 560 | ->where('parleyable_type', get_class($member)) 561 | ->update(['notified' => 0]); 562 | } 563 | 564 | return true; 565 | } 566 | 567 | /** 568 | * Create a new messageData object for this thread 569 | * 570 | * @param array $messageData 571 | * @return Message 572 | * @throws InvalidMessageFormatException 573 | */ 574 | protected function createMessage(array $messageData) 575 | { 576 | // We can't proceed if there is no messageData body. 577 | if (! array_key_exists('body', $messageData)) { 578 | throw new InvalidMessageFormatException("Missing body from message data attributes"); 579 | } 580 | 581 | // Assemble the Message components and create the messageData 582 | $message = Message::create([ 583 | 'body' => e($messageData['body']), 584 | 'parley_thread_id' => $this->id 585 | ]); 586 | 587 | // Set the message author and author_alias 588 | $alias = array_key_exists('alias', $messageData) ? $messageData['alias'] : $messageData['author']->parley_alias; 589 | $message->setAuthor($messageData['author'], $alias); 590 | 591 | // Create the Message Object 592 | return $message; 593 | } 594 | } 595 | -------------------------------------------------------------------------------- /src/ParleyManager.php: -------------------------------------------------------------------------------- 1 | confirmObjectIsParleyable($messageData['author']); 41 | 42 | // Create a new Parley Thread with its first Message 43 | $thread = Thread::create(['subject' => e($messageData['subject'])]); 44 | $thread->setInitialMessage($messageData); 45 | 46 | // Set the reference object if it has been provided in the message data 47 | if (array_key_exists('regarding', $messageData)) { 48 | $thread->setReferenceObject($messageData['regarding']); 49 | } 50 | 51 | // Set the reference object if it has been passed as a parameter to the discuss method 52 | if ($object) { 53 | $thread->setReferenceObject($object); 54 | } 55 | 56 | return $thread; 57 | } 58 | 59 | /** 60 | * Gather Threads for a group of objects 61 | * 62 | * @param mixed $members 63 | * 64 | * @return ParleySelector 65 | */ 66 | public function gatherFor($members) 67 | { 68 | $members = $this->ensureArrayable($members); 69 | 70 | return new ParleySelector($members); 71 | } 72 | 73 | /** 74 | * Get a thread by its hash value 75 | * @param string|int $id 76 | * 77 | * @return mixed 78 | */ 79 | public function getThread($id) 80 | { 81 | return Thread::find($id); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/ParleyServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 26 | __DIR__.'/../migrations/' => database_path('migrations') 27 | ], 'migrations'); 28 | } 29 | 30 | /** 31 | * Register the service provider. 32 | * 33 | * @return void 34 | */ 35 | public function register() 36 | { 37 | // Register the Parley Manager to the IOC Container 38 | $this->app->singleton('parley',function ($app) { 39 | return new ParleyManager; 40 | }); 41 | } 42 | 43 | /** 44 | * Get the services provided by the provider. 45 | * 46 | * @return array 47 | */ 48 | public function provides() 49 | { 50 | return ['parley']; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Support/ParleySelector.php: -------------------------------------------------------------------------------- 1 | members = $members; 22 | } 23 | 24 | /** 25 | * Include "deleted" threads in the selection 26 | * 27 | * @return $this 28 | */ 29 | public function withTrashed() 30 | { 31 | $this->trashed = 'yes'; 32 | 33 | return $this; 34 | } 35 | 36 | /** 37 | * Select only threads that have been "deleted" (aka soft-deleted) 38 | * 39 | * @return $this 40 | */ 41 | public function onlyTrashed() 42 | { 43 | $this->trashed = 'only'; 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * Select only threads that have been read 50 | * 51 | * @return $this 52 | */ 53 | public function read() 54 | { 55 | $this->status = 'read'; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * Select only unread threads 62 | * 63 | * @return $this 64 | */ 65 | public function unread() 66 | { 67 | $this->status = 'unread'; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Select only threads that are "open" 74 | * 75 | * @return $this 76 | */ 77 | public function open() 78 | { 79 | $this->type = 'open'; 80 | 81 | return $this; 82 | } 83 | 84 | public function closed() 85 | { 86 | $this->type = 'closed'; 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * Return a count of the threads that have been selected 93 | * 94 | * @return int 95 | */ 96 | public function count() 97 | { 98 | $count = 0; 99 | 100 | foreach ($this->members as $member) { 101 | $count += $this->getThreadsForMember($member)->count(); 102 | } 103 | 104 | return $count; 105 | } 106 | 107 | /** 108 | * Return a collection of the threads that have been selected 109 | * 110 | * @return Collection 111 | */ 112 | public function get() 113 | { 114 | $results = new Collection(); 115 | 116 | foreach ($this->members as $member) { 117 | $results = $results->merge($this->getThreadsForMember($member)); 118 | } 119 | 120 | return $results; 121 | } 122 | 123 | /** 124 | * Get Threads belonging to a specific Member object 125 | * 126 | * @param $member 127 | * 128 | * @return mixed 129 | */ 130 | public function getThreadsForMember($member) 131 | { 132 | // Confirm the specified member is a valid Parleyable Object 133 | if (! $this->confirmObjectIsParleyable($member)) { 134 | return new Collection(); 135 | } 136 | 137 | $query = Thread::join('parley_members', 'parley_threads.id', '=', 'parley_members.parley_thread_id') 138 | ->where('parley_members.parleyable_id', $member->id) 139 | ->where('parley_members.parleyable_type', get_class($member)) 140 | ->select('parley_threads.*', 141 | 'parley_members.is_read as is_read', 142 | 'parley_members.parleyable_id as member_id', 143 | 'parley_members.parleyable_type as member_type'); 144 | 145 | switch ($this->trashed) { 146 | case 'yes': 147 | $query = $query->withTrashed(); 148 | break; 149 | 150 | case 'only': 151 | $query = $query->onlyTrashed(); 152 | break; 153 | 154 | default: 155 | break; 156 | } 157 | 158 | if ($this->type == 'open') { 159 | $query = $query->whereNull('parley_threads.closed_at'); 160 | } 161 | 162 | if ($this->type == 'closed') { 163 | $query = $query->whereNotNull('parley_threads.closed_at'); 164 | } 165 | 166 | if ($this->status == 'read') { 167 | $query = $query->where('parley_members.is_read', 1); 168 | } 169 | 170 | if ($this->status == 'unread') { 171 | $query = $query->where('parley_members.is_read', 0); 172 | } 173 | 174 | return $query->orderBy('updated_at', 'desc')->get(); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/Traits/ParleyHelpersTrait.php: -------------------------------------------------------------------------------- 1 | getKey())) { 44 | return true; 45 | } 46 | 47 | if (! $silent) { 48 | throw new NonReferableObjectException; 49 | } 50 | 51 | return null; 52 | } 53 | 54 | /** 55 | * Convert an unknown entity (or entities) into a flattened array. 56 | * 57 | * @param $group 58 | * @return array 59 | */ 60 | protected function ensureArrayable($group) 61 | { 62 | if (! is_array($group)) { 63 | $group = [$group]; 64 | } 65 | 66 | return $group; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/ParleyManagerTests.php: -------------------------------------------------------------------------------- 1 | 'Happy Name Day!', 17 | 'body' => "Congratulations on your 20th name day!", 18 | 'alias' => $this->nikolai->alias, 19 | 'author' => $this->nikolai 20 | ])->withParticipant($this->irina); 21 | 22 | sleep(2); 23 | 24 | $parley->reply([ 25 | 'body' => "I am feeling so very old today.", 26 | 'author' => $this->irina 27 | ]); 28 | 29 | $members = $parley->getMembers(); 30 | $newestMessage = $parley->newestMessage(); 31 | $originalMessage = $parley->originalMessage(); 32 | 33 | $this->assertInstanceOf('Parley\Models\Thread', $parley); 34 | $this->assertEquals($parley->subject, 'Happy Name Day!'); 35 | $this->assertEquals(2, $parley->messages()->count()); 36 | 37 | $this->assertInstanceOf('Illuminate\Support\Collection', $members); 38 | $this->assertEquals(2, $members->count()); 39 | 40 | $this->assertEquals('Congratulations on your 20th name day!', $originalMessage->body); 41 | $this->assertInstanceOf('Parley\Models\Message', $newestMessage); 42 | $this->assertEquals('I am feeling so very old today.', $newestMessage->body); 43 | } 44 | 45 | public function test_parley_discussion_with_reference_object() 46 | { 47 | $widgetObject = Widget::create(['name' => 'Gift']); 48 | 49 | $parley = Parley::discuss([ 50 | 'subject' => 'Happy Name Day!', 51 | 'body' => "Congratulations on your 20th name day!", 52 | 'alias' => $this->irina->alias, 53 | 'author' => $this->irina 54 | ], $widgetObject)->withParticipants($this->nikolai); 55 | 56 | $this->assertInstanceOf('Parley\Models\Thread', $parley); 57 | $this->assertInstanceOf('Chekhov\Widget', $parley->getReferenceObject()); 58 | $this->assertEquals($parley->subject, 'Happy Name Day!'); 59 | } 60 | 61 | public function test_parley_discussion_with_reference_object_in_message() 62 | { 63 | $widgetObject = Widget::create(['name' => 'Gift']); 64 | 65 | $parley = Parley::discuss([ 66 | 'subject' => 'Happy Name Day!', 67 | 'body' => "Congratulations on your 20th name day!", 68 | 'alias' => $this->irina->alias, 69 | 'author' => $this->irina, 70 | 'regarding' => $widgetObject 71 | ])->withParticipants($this->nikolai); 72 | 73 | $this->assertInstanceOf('Parley\Models\Thread', $parley); 74 | $this->assertInstanceOf('Chekhov\Widget', $parley->getReferenceObject()); 75 | $this->assertEquals($parley->subject, 'Happy Name Day!'); 76 | } 77 | 78 | public function test_gathering_member_threads() 79 | { 80 | // Parley #1 81 | $parley1 = $this->simulate_a_conversation("Happy Name Day!"); 82 | $parley1->reply([ 83 | 'body' => "Nonsense - you should be celebrating!", 84 | 'author' => $this->nikolai 85 | ]); 86 | // Parley #2 87 | $parley2 = $this->simulate_a_conversation("My thoughts on our future society"); 88 | $parley2->markReadForMembers($this->irina); 89 | // Parley #3 90 | $parley3 = $this->simulate_a_conversation("Regiment Newsletter"); 91 | $parley3->closedBy($this->irina); 92 | // Parley #4 93 | $parley4 = $this->simulate_a_conversation("Pay no attention to Solyony"); 94 | $parley4->delete(); 95 | // Parley #5 96 | Parley::discuss([ 97 | 'subject' => 'You are Invited', 98 | 'body' => "Please join us for dinner this evening at our residence.", 99 | 'author' => $this->prozorovGroup 100 | ])->withParticipants($this->nikolai); 101 | 102 | $irinaThreads = Parley::gatherFor($this->irina)->get(); 103 | $irinaThreadsCount = Parley::gatherFor($this->irina)->count(); 104 | $irinaOpenThreads = Parley::gatherFor($this->irina)->open()->get(); 105 | $irinaClosedThreads = Parley::gatherFor($this->irina)->closed()->get(); 106 | $irinaThreadsWithTrashed = Parley::gatherFor($this->irina)->withTrashed()->get(); 107 | $irinaThreadsOnlyTrashed = Parley::gatherFor($this->irina)->onlyTrashed()->get(); 108 | $irinaUnreadThreadCount = Parley::gatherFor($this->irina)->unread()->count(); 109 | $irinaReadThreadCount = Parley::gatherFor($this->irina)->read()->count(); 110 | $nikolaiUnreadThreadCount = Parley::gatherFor($this->nikolai)->unread()->count(); 111 | $nikolaiReadThreadCount = Parley::gatherFor($this->nikolai)->read()->count(); 112 | $multiGatherThreads = Parley::gatherFor([$this->irina, $this->prozorovGroup])->get(); 113 | $multiGatherUnread = Parley::gatherFor([$this->irina, $this->prozorovGroup])->unread()->get(); 114 | 115 | $this->assertEquals(3, $irinaThreads->count()); 116 | $this->assertEquals($irinaThreadsCount, $irinaThreads->count()); 117 | $this->assertEquals(2, $irinaOpenThreads->count()); 118 | $this->assertEquals(1, $irinaClosedThreads->count()); 119 | $this->assertEquals(4, $irinaThreadsWithTrashed->count()); 120 | $this->assertEquals(1, $irinaThreadsOnlyTrashed->count()); 121 | $this->assertEquals(1, $irinaUnreadThreadCount); 122 | $this->assertEquals(2, $irinaReadThreadCount); 123 | $this->assertEquals(3, $nikolaiUnreadThreadCount); 124 | $this->assertEquals(1, $nikolaiReadThreadCount); 125 | $this->assertEquals(4, $multiGatherThreads->count()); 126 | $this->assertEquals(1, $multiGatherUnread->count()); 127 | $this->assertInstanceOf('Illuminate\Support\Collection', $multiGatherThreads); 128 | $this->assertInstanceOf('Illuminate\Support\Collection', $irinaThreads); 129 | } 130 | 131 | public function test_gathering_threads_for_invalid_member() 132 | { 133 | $this->expectException(\Parley\Exceptions\NonParleyableMemberException::class); 134 | 135 | $this->simulate_a_conversation("Happy Name Day!"); 136 | 137 | $group = null; 138 | 139 | $parleys = Parley::gatherFor($group)->get(); 140 | 141 | $this->assertInstanceOf('Parley\Support\Collection', $parleys); 142 | $this->assertEquals(0, $parleys->count()); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tests/ParleyMessageTests.php: -------------------------------------------------------------------------------- 1 | 'Happy Name Day!', 14 | 'body' => 'Congratulations on your 20th name day!', 15 | 'alias' => 'Baron Nikolaj Lvovich Tuzenbach', 16 | 'author' => $this->nikolai 17 | ])->withParticipants($this->irina); 18 | 19 | sleep(1); 20 | 21 | $parley->reply([ 22 | 'body' => 'I am feeling so very old today.', 23 | 'alias' => 'Irina Sergeyevna Prozorova', 24 | 'author' => $this->irina 25 | ]); 26 | 27 | $initialMessage = $parley->originalMessage(); 28 | $replyMessage = $parley->newestMessage(); 29 | 30 | $this->assertInstanceOf('Parley\Models\Message', $initialMessage); 31 | $this->assertEquals($initialMessage->body, 'Congratulations on your 20th name day!'); 32 | $this->assertEquals($initialMessage->author_alias, 'Baron Nikolaj Lvovich Tuzenbach'); 33 | $this->assertNotEquals($initialMessage->author_alias, $this->nikolai->alias); 34 | 35 | $this->assertInstanceOf('Parley\Models\Message', $replyMessage); 36 | $this->assertEquals($replyMessage->body, 'I am feeling so very old today.'); 37 | $this->assertEquals($replyMessage->author_alias, 'Irina Sergeyevna Prozorova'); 38 | $this->assertNotEquals($replyMessage->author_alias, $this->irina->alias); 39 | } 40 | 41 | public function test_creating_messages_without_explicit_alias() 42 | { 43 | $parley = Parley::discuss([ 44 | 'subject' => 'Happy Name Day!', 45 | 'body' => 'Congratulations on your 20th name day!', 46 | 'author' => $this->nikolai 47 | ])->withParticipants($this->irina); 48 | 49 | sleep(1); 50 | 51 | $parley->reply([ 52 | 'body' => 'I am feeling so very old today.', 53 | 'author' => $this->irina 54 | ]); 55 | 56 | $initialMessage = $parley->originalMessage(); 57 | $replyMessage = $parley->newestMessage(); 58 | 59 | $this->assertInstanceOf('Parley\Models\Message', $initialMessage); 60 | $this->assertEquals($initialMessage->author_alias, $this->nikolai->getParleyAliasAttribute()); 61 | 62 | $this->assertInstanceOf('Parley\Models\Message', $replyMessage); 63 | $this->assertEquals($replyMessage->author_alias, $this->irina->getParleyAliasAttribute()); 64 | } 65 | 66 | public function test_setting_and_retrieving_message_author() 67 | { 68 | $parley = $this->simulate_a_conversation(); 69 | $message = $parley->newestMessage(); 70 | 71 | $originalAuthor = $message->getAuthor(); 72 | 73 | $message->setAuthor($this->prozorovGroup); 74 | 75 | $updatedAuthor = $message->getAuthor(); 76 | 77 | $this->assertInstanceOf('Chekhov\User', $originalAuthor); 78 | $this->assertEquals('Irina Prozorovna', $originalAuthor->getParleyAliasAttribute()); 79 | $this->assertInstanceOf('Chekhov\Group', $updatedAuthor); 80 | $this->assertEquals('The Prozorovs', $updatedAuthor->getParleyAliasAttribute()); 81 | } 82 | 83 | public function test_retrieve_thread_from_message() 84 | { 85 | $parley = $this->simulate_a_conversation(); 86 | $message = $parley->newestMessage(); 87 | $thread = $message->thread; 88 | 89 | $this->assertInstanceOf('Parley\Models\Thread', $thread); 90 | $this->assertEquals('Happy Name Day!', $thread->subject); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/ParleyTestCase.php: -------------------------------------------------------------------------------- 1 | irina = User::create(['email' => 'irina@prozorov.net', 'first_name' => 'Irina', 'last_name' => 'Prozorovna']); 28 | $this->nikolai = User::create(['email' => 'nikolai@tuzenbach.com', 'first_name' => 'Nikolai', 'last_name' => 'Tuzenbach']); 29 | $this->prozorovGroup = Group::create(['name' => 'The Prozorovs']); 30 | } 31 | 32 | /** 33 | * Define environment setup. 34 | * 35 | * @param Illuminate\Foundation\Application $app 36 | * 37 | * @return void 38 | */ 39 | protected function getEnvironmentSetUp($app) 40 | { 41 | // Setup default database to use sqlite :memory: 42 | $app['config']->set('database.default', 'testbench'); 43 | $app['config']->set('database.connections.testbench', [ 44 | 'driver' => 'sqlite', 45 | 'database' => __DIR__ . '/_data/db/database.sqlite', 46 | 'prefix' => '', 47 | ]); 48 | } 49 | 50 | /** 51 | * Get package providers. At a minimum this is the package being tested, but also 52 | * would include packages upon which our package depends, e.g. Cartalyst/Sentry 53 | * In a normal app environment these would be added to the 'providers' array in 54 | * the config/app.php file. 55 | * 56 | * @return array 57 | */ 58 | protected function getPackageProviders($app) 59 | { 60 | return array( 61 | 'Parley\ParleyServiceProvider', 62 | ); 63 | } 64 | 65 | /** 66 | * Get package aliases. In a normal app environment these would be added to 67 | * the 'aliases' array in the config/app.php file. If your package exposes an 68 | * aliased facade, you should add the alias here, along with aliases for 69 | * facades upon which your package depends, e.g. Cartalyst/Sentry 70 | * 71 | * @return array 72 | */ 73 | protected function getPackageAliases($app) 74 | { 75 | return array( 76 | 'Parley' => 'Parley\Facades\Parley', 77 | ); 78 | } 79 | 80 | /** 81 | * Call artisan command and return code. 82 | * 83 | * @param string $command 84 | * @param array $parameters 85 | * 86 | * @return int 87 | */ 88 | public function artisan($command, $parameters = []) 89 | { 90 | // TODO: Implement artisan() method. 91 | } 92 | 93 | /** 94 | * A helper method for quickly stubbing out parley conversations 95 | * 96 | * @param $subject 97 | * @return mixed 98 | */ 99 | protected function simulate_a_conversation($subject = 'Happy Name Day!') 100 | { 101 | $parley = Parley::discuss([ 102 | 'subject' => $subject, 103 | 'body' => 'Congratulations on your 20th name day!', 104 | 'alias' => $this->nikolai->alias, 105 | 'author' => $this->nikolai 106 | ])->withParticipants($this->irina); 107 | 108 | sleep(1); 109 | 110 | $parley->reply([ 111 | 'body' => 'I am feeling so very old today.', 112 | 'author' => $this->irina 113 | ]); 114 | 115 | return $parley; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/ParleyThreadTests.php: -------------------------------------------------------------------------------- 1 | simulate_a_conversation('Happy Name Day!'); 13 | 14 | $this->assertInstanceOf(\Parley\Models\Thread::class, $parley); 15 | $this->assertEquals($parley->subject, 'Happy Name Day!'); 16 | } 17 | 18 | public function test_adding_member_to_thread() 19 | { 20 | $parley = $this->simulate_a_conversation('Happy Name Day!'); 21 | $parley->addParticipant($this->prozorovGroup); 22 | $members = $parley->getMembers(); 23 | 24 | $this->assertEquals($members->count(), 3); 25 | } 26 | 27 | public function test_adding_members_via_with_participants_method() 28 | { 29 | Event::fake(); 30 | 31 | $parley = Parley::discuss([ 32 | 'subject' => 'Happy Name Day!', 33 | 'body' => 'Congratulations on your 20th name day!', 34 | 'alias' => $this->nikolai->getParleyAliasAttribute(), 35 | 'author' => $this->nikolai 36 | ])->withParticipants([$this->irina, $this->prozorovGroup]); 37 | 38 | $members = $parley->getMembers(); 39 | 40 | $this->assertEquals($members->count(), 3); 41 | Event::assertDispatched(\Parley\Events\ParleyThreadCreated::class); 42 | } 43 | 44 | public function test_adding_a_collection_of_members_to_a_thread() 45 | { 46 | Event::fake(); 47 | 48 | $users = User::get(); 49 | 50 | $parley = Parley::discuss([ 51 | 'subject' => 'You are Invited', 52 | 'body' => "Please join us for dinner this evening at our residence.", 53 | 'author' => $this->prozorovGroup 54 | ])->withParticipants($users); 55 | 56 | $members = $parley->getMembers(); 57 | 58 | $this->assertEquals($members->count(), 3); 59 | Event::assertDispatched(\Parley\Events\ParleyThreadCreated::class); 60 | } 61 | 62 | public function test_adding_an_array_of_members_to_a_thread() 63 | { 64 | Event::fake(); 65 | 66 | $users = User::get()->all(); 67 | 68 | $parley = Parley::discuss([ 69 | 'subject' => 'You are Invited', 70 | 'body' => "Please join us for dinner this evening at our residence.", 71 | 'author' => $this->prozorovGroup 72 | ])->withParticipants($users); 73 | 74 | $members = $parley->getMembers(); 75 | 76 | $this->assertEquals($members->count(), 3); 77 | Event::assertDispatched(\Parley\Events\ParleyThreadCreated::class); 78 | } 79 | 80 | public function test_adding_members_to_a_thread_via_multiple_arguments() 81 | { 82 | Event::fake(); 83 | 84 | $aleksandr = User::create(['email' => 'aleksandr@vershinin.com']); 85 | 86 | $parley = Parley::discuss([ 87 | 'subject' => 'You are Invited', 88 | 'body' => "Please join us for dinner this evening at our residence.", 89 | 'author' => $this->prozorovGroup 90 | ])->withParticipants($this->irina, $this->nikolai, $aleksandr); 91 | 92 | $members = $parley->getMembers(); 93 | 94 | $this->assertEquals($members->count(), 4); 95 | Event::assertDispatched(\Parley\Events\ParleyThreadCreated::class); 96 | } 97 | 98 | public function test_adding_nonparleyable_object_as_member() 99 | { 100 | $this->expectException(\Parley\Exceptions\NonParleyableMemberException::class); 101 | 102 | $widget = Widget::create(['name' => 'Gift']); 103 | 104 | Parley::discuss([ 105 | 'subject' => 'Happy Name Day!', 106 | 'body' => 'Congratulations on your 20th name day!', 107 | 'alias' => $this->nikolai->getParleyAliasAttribute(), 108 | 'author' => $this->nikolai 109 | ])->withParticipants($widget); 110 | } 111 | 112 | public function test_removing_member_from_thread() 113 | { 114 | $parley = Parley::discuss([ 115 | 'subject' => 'Happy Name Day!', 116 | 'body' => 'Congratulations on your 20th name day!', 117 | 'alias' => $this->nikolai->getParleyAliasAttribute(), 118 | 'author' => $this->nikolai 119 | ])->withParticipants([$this->irina, $this->prozorovGroup]); 120 | 121 | $parley->removeParticipant($this->prozorovGroup); 122 | 123 | $members = $parley->getMembers(); 124 | 125 | $this->assertEquals($members->count(), 2); 126 | } 127 | 128 | public function test_validating_thread_membership() 129 | { 130 | $parley = $this->simulate_a_conversation('Happy Name Day!'); 131 | 132 | $this->assertTrue($parley->isMember($this->irina)); 133 | $this->assertFalse($parley->isMember($this->prozorovGroup)); 134 | } 135 | 136 | public function test_adding_and_removing_reference_object() 137 | { 138 | $pencils = Widget::create(['name' => 'Pencils']); 139 | $penknife = Widget::create(['name' => 'Penknife']); 140 | 141 | $parley = Parley::discuss([ 142 | 'subject' => 'Happy Name Day!', 143 | 'body' => 'Congratulations on your 20th name day!', 144 | 'alias' => $this->nikolai->getParleyAliasAttribute(), 145 | 'author' => $this->nikolai 146 | ], $pencils)->withParticipants([$this->irina, $this->prozorovGroup]); 147 | 148 | $originalReferenceObject = $parley->getReferenceObject(); 149 | 150 | $parley->clearReferenceObject(); 151 | $removedReferenceObject = $parley->getReferenceObject(); 152 | 153 | $parley->setReferenceObject($penknife); 154 | $newReferenceObject = $parley->getReferenceObject(); 155 | 156 | $this->assertInstanceOf('Chekhov\Widget', $originalReferenceObject); 157 | $this->assertEquals('Pencils', $originalReferenceObject->name); 158 | $this->assertNull($removedReferenceObject); 159 | $this->assertInstanceOf('Chekhov\Widget', $newReferenceObject); 160 | $this->assertEquals('Penknife', $newReferenceObject->name); 161 | } 162 | 163 | public function test_adding_object_without_id_as_reference_object() 164 | { 165 | $this->expectException(\Parley\Exceptions\NonReferableObjectException::class); 166 | 167 | $widget = new Widget(); 168 | 169 | Parley::discuss([ 170 | 'subject' => 'Happy Name Day!', 171 | 'body' => 'Congratulations on your 20th name day!', 172 | 'alias' => $this->nikolai->getParleyAliasAttribute(), 173 | 'author' => $this->nikolai 174 | ], $widget)->withParticipants($this->irina); 175 | } 176 | 177 | public function test_opening_and_closing_a_thread() 178 | { 179 | $parley = Parley::discuss([ 180 | 'subject' => 'Happy Name Day!', 181 | 'body' => 'Congratulations on your 20th name day!', 182 | 'alias' => $this->nikolai->getParleyAliasAttribute(), 183 | 'author' => $this->nikolai 184 | ])->withParticipants([$this->irina, $this->prozorovGroup]); 185 | 186 | $parley->closedBy($this->irina); 187 | $shouldBeClosed = $parley->isClosed(); 188 | 189 | $parley->reopen(); 190 | $shouldBeOpen = $parley->isClosed(); 191 | 192 | $this->assertTrue($shouldBeClosed); 193 | $this->assertFalse($shouldBeOpen); 194 | } 195 | 196 | public function test_retrieving_member_who_closed_a_thread() 197 | { 198 | $parley = Parley::discuss([ 199 | 'subject' => 'Happy Name Day!', 200 | 'body' => 'Congratulations on your 20th name day!', 201 | 'alias' => $this->nikolai->getParleyAliasAttribute(), 202 | 'author' => $this->nikolai 203 | ])->withParticipants([$this->irina, $this->prozorovGroup]); 204 | 205 | $parley->closedBy($this->irina); 206 | $closer = $parley->getCloser(); 207 | 208 | $this->assertTrue($parley->isClosed()); 209 | $this->assertInstanceOf('Chekhov\User', $closer); 210 | $this->assertEquals('Irina Prozorovna', $closer->getParleyAliasAttribute()); 211 | } 212 | 213 | public function test_retrieving_the_newest_message_in_a_thread() 214 | { 215 | $parley = $this->simulate_a_conversation(); 216 | sleep(1); 217 | $parley->reply([ 218 | 'body' => "Nonsense - you should be celebrating!", 219 | 'author' => $this->nikolai 220 | ]); 221 | 222 | $message = $parley->newestMessage(); 223 | 224 | $this->assertInstanceOf('Parley\Models\Message', $message); 225 | $this->assertEquals("Nonsense - you should be celebrating!", $message->body); 226 | } 227 | 228 | public function test_retrieving_the_original_message_in_a_thread() 229 | { 230 | $parley = $this->simulate_a_conversation(); 231 | sleep(1); 232 | $parley->reply([ 233 | 'body' => "Nonsense - you should be celebrating!", 234 | 'author' => $this->nikolai 235 | ]); 236 | 237 | $message = $parley->originalMessage(); 238 | 239 | $this->assertInstanceOf('Parley\Models\Message', $message); 240 | $this->assertEquals("Congratulations on your 20th name day!", $message->body); 241 | } 242 | 243 | public function test_retrieving_all_thread_messages() 244 | { 245 | $parley = $this->simulate_a_conversation(); 246 | sleep(1); 247 | $parley->reply([ 248 | 'body' => "Nonsense - you should be celebrating!", 249 | 'author' => $this->nikolai 250 | ]); 251 | 252 | $messages = $parley->messages; 253 | 254 | $this->assertInstanceOf('Illuminate\Support\Collection', $messages); 255 | $this->assertEquals(3, $messages->count()); 256 | } 257 | 258 | public function test_marking_read_and_unread_for_individual_members() 259 | { 260 | $parley = $this->simulate_a_conversation(); 261 | 262 | $irinaHasReadA = $parley->hasBeenReadByMember($this->irina); // Should be true 263 | $nikolaiHasReadA = $parley->hasBeenReadByMember($this->nikolai); // Should be false 264 | 265 | $parley->reply([ 266 | 'body' => "Nonsense - you should be celebrating!", 267 | 'author' => $this->nikolai 268 | ]); 269 | 270 | $irinaHasReadB = $parley->hasBeenReadByMember($this->irina); // Should be false 271 | $nikolaiHasReadB = $parley->hasBeenReadByMember($this->nikolai); // Should be true 272 | 273 | $parley->markUnreadForMembers($this->nikolai); 274 | $nikolaiHasReadC = $parley->hasBeenReadByMember($this->nikolai); // Should be false 275 | 276 | $parley->markReadForMembers($this->irina); 277 | $irinaHasReadC = $parley->hasBeenReadByMember($this->irina); // Should be true 278 | 279 | $this->assertTrue($irinaHasReadA); 280 | $this->assertFalse($nikolaiHasReadA); 281 | $this->assertFalse($irinaHasReadB); 282 | $this->assertTrue($nikolaiHasReadB); 283 | $this->assertTrue($irinaHasReadC); 284 | $this->assertFalse($nikolaiHasReadC); 285 | } 286 | 287 | public function test_retrieving_all_thread_members() 288 | { 289 | $parley = $this->simulate_a_conversation(); 290 | $members = $parley->getMembers(); 291 | 292 | $this->assertInstanceOf('Illuminate\Support\Collection', $members); 293 | $this->assertEquals(2, $members->count()); 294 | } 295 | 296 | public function test_retrieving_filtered_thread_members() 297 | { 298 | $parley = $this->simulate_a_conversation(); 299 | $members = $parley->getMembers(['except' => $this->nikolai]); 300 | 301 | $this->assertInstanceOf('Illuminate\Support\Collection', $members); 302 | $this->assertEquals(1, $members->count()); 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /tests/TestingEnvironmentTests.php: -------------------------------------------------------------------------------- 1 | insert(array( 12 | 'subject' => 'test thread', 13 | 'created_at' => date("Y-m-d H:i:s"), 14 | 'updated_at' => date("Y-m-d H:i:s") 15 | )); 16 | 17 | $threads = Thread::all(); 18 | $this->assertEquals($threads->count(), 1); 19 | $this->assertEquals($threads->first()->id, 1); 20 | } 21 | 22 | public function test_object_instantiation() 23 | { 24 | $widget = Widget::create(['name' => 'Test Widget']); 25 | $this->assertInstanceOf('Chekhov\Widget', $widget); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/_data/Group.php: -------------------------------------------------------------------------------- 1 | name; 14 | } 15 | 16 | /** 17 | * Each Parleyable object must provide an integer id value. Usually this is can be 18 | * as simple as "return $this->attributes['id'];". 19 | * 20 | * @return int 21 | */ 22 | public function getParleyIdAttribute() 23 | { 24 | return $this->attributes['id']; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/_data/User.php: -------------------------------------------------------------------------------- 1 | first_name . ' ' . $this->last_name; 14 | } 15 | 16 | /** 17 | * Each Parleyable object must provide an integer id value. Usually this is can be 18 | * as simple as "return $this->attributes['id'];". 19 | * 20 | * @return int 21 | */ 22 | public function getParleyIdAttribute() 23 | { 24 | return $this->attributes['id']; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/_data/Widget.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('email')->nullable(); 19 | $table->string('first_name')->nullable(); 20 | $table->string('last_name')->nullable(); 21 | $table->timestamps(); 22 | }); 23 | 24 | Schema::create('groups', function($table){ 25 | $table->increments('id'); 26 | $table->string('name')->nullable(); 27 | $table->timestamps(); 28 | }); 29 | 30 | Schema::create('widgets', function($table){ 31 | $table->increments('id'); 32 | $table->string('name')->nullable(); 33 | $table->timestamps(); 34 | }); 35 | } 36 | 37 | /** 38 | * Reverse the migrations. 39 | * 40 | * @return void 41 | */ 42 | public function down() 43 | { 44 | // Drop TestData tables 45 | Schema::drop('users'); 46 | Schema::drop('groups'); 47 | Schema::drop('widgets'); 48 | } 49 | 50 | } 51 | --------------------------------------------------------------------------------