From e921bf0acb61e93664cc0508d0edded789a036c9 Mon Sep 17 00:00:00 2001 From: Joris Vaesen Date: Wed, 17 Feb 2016 21:50:35 +0100 Subject: [PATCH 1/8] Option to delete file on record deletion --- src/File/Writer/DefaultWriter.php | 17 +++++++++++ src/File/Writer/WriterInterface.php | 8 ++++++ src/Model/Behavior/UploadBehavior.php | 28 +++++++++++++++++++ .../Model/Behavior/UploadBehaviorTest.php | 9 ++++++ 4 files changed, 62 insertions(+) diff --git a/src/File/Writer/DefaultWriter.php b/src/File/Writer/DefaultWriter.php index 547f969d..0d8bb550 100644 --- a/src/File/Writer/DefaultWriter.php +++ b/src/File/Writer/DefaultWriter.php @@ -84,6 +84,23 @@ public function write(array $files) return $results; } + /** + * Deletes a set of files to an output + * + * @param array $files the files being written out + * @return array array of results + */ + public function delete(array $files) + { + $filesystem = $this->getFilesystem($this->field, $this->settings); + $results = []; + foreach ($files as $path) { + $results[] = $this->deletePath($filesystem, $path); + } + + return $results; + } + /** * Writes a set of files to an output * diff --git a/src/File/Writer/WriterInterface.php b/src/File/Writer/WriterInterface.php index e44e832c..48ce243d 100644 --- a/src/File/Writer/WriterInterface.php +++ b/src/File/Writer/WriterInterface.php @@ -24,4 +24,12 @@ public function __construct(Table $table, Entity $entity, $data, $field, $settin * @return array array of results */ public function write(array $files); + + /** + * Deletes a set of files to an output + * + * @param array $files the files being written out + * @return array array of results + */ + public function delete(array $files); } diff --git a/src/Model/Behavior/UploadBehavior.php b/src/Model/Behavior/UploadBehavior.php index 9d8c8d78..ce61af2e 100644 --- a/src/Model/Behavior/UploadBehavior.php +++ b/src/Model/Behavior/UploadBehavior.php @@ -95,6 +95,34 @@ public function beforeSave(Event $event, Entity $entity, ArrayObject $options) } } + /** + * Deletes the files after the entity is deleted + * + * @param \Cake\Event\Event $event The afterDelete event that was fired + * @param \Cake\ORM\Entity $entity The entity that was deleted + * @param \ArrayObject $options the options passed to the delete method + * @return void|false + */ + public function afterDelete(Event $event, Entity $entity, ArrayObject $options) + { + foreach ($this->config() as $field => $settings) + { + if (Hash::get($settings, 'keepFilesOnDelete', true)) + { + continue; + } + + $file = [$entity->{$settings['dir']} . $entity->{$field}]; + $writer = $this->getWriter($entity, [], $field, $settings); + $success = $writer->delete($file); + + if ((new Collection($success))->contains(false)) + { + return false; + } + } + } + /** * Retrieves an instance of a path processor which knows how to build paths * for a given file upload diff --git a/tests/TestCase/Model/Behavior/UploadBehaviorTest.php b/tests/TestCase/Model/Behavior/UploadBehaviorTest.php index 8fc2853f..a46beb44 100644 --- a/tests/TestCase/Model/Behavior/UploadBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/UploadBehaviorTest.php @@ -201,6 +201,15 @@ public function testBeforeSaveOk() $this->assertNull($behavior->beforeSave(new Event('fake.event'), $this->entity, new ArrayObject)); } + public function testAfterDelete() + { + $methods = array_diff($this->behaviorMethods, ['config', 'afterDelete']); + $behavior = $this->getMock('Josegonzalez\Upload\Model\Behavior\UploadBehavior', $methods, [$this->table, $this->settings]); + $behavior->config($this->settings); + + $this->assertNull($behavior->afterDelete(new Event('fake.event'), $this->entity, new ArrayObject)); + } + public function testGetWriter() { $processor = $this->behavior->getWriter($this->entity, [], 'field', []); From 165a1e5bd5a83b35b91bb3e626822f5a46fc4063 Mon Sep 17 00:00:00 2001 From: Joris Vaesen Date: Wed, 17 Feb 2016 22:04:43 +0100 Subject: [PATCH 2/8] phpcs fix and docs update --- docs/configuration.rst | 4 ++++ src/Model/Behavior/UploadBehavior.php | 9 +++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 588a1849..05ed1297 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -61,3 +61,7 @@ passed in under each field in your behavior configuration. - ``array $settings``: UploadBehavior settings for the current field - Return: (string) the new name for the file + +- ``keepFilesOnDelete``: Keep *all* files when uploading/deleting a record. + + - Default: (boolean) ``true`` diff --git a/src/Model/Behavior/UploadBehavior.php b/src/Model/Behavior/UploadBehavior.php index ce61af2e..e79bd95e 100644 --- a/src/Model/Behavior/UploadBehavior.php +++ b/src/Model/Behavior/UploadBehavior.php @@ -105,10 +105,8 @@ public function beforeSave(Event $event, Entity $entity, ArrayObject $options) */ public function afterDelete(Event $event, Entity $entity, ArrayObject $options) { - foreach ($this->config() as $field => $settings) - { - if (Hash::get($settings, 'keepFilesOnDelete', true)) - { + foreach ($this->config() as $field => $settings) { + if (Hash::get($settings, 'keepFilesOnDelete', true)) { continue; } @@ -116,8 +114,7 @@ public function afterDelete(Event $event, Entity $entity, ArrayObject $options) $writer = $this->getWriter($entity, [], $field, $settings); $success = $writer->delete($file); - if ((new Collection($success))->contains(false)) - { + if ((new Collection($success))->contains(false)) { return false; } } From ed5c82f7aea142c2f4a77a9dc0afd8f1b41b7b9b Mon Sep 17 00:00:00 2001 From: Joris Vaesen Date: Wed, 17 Feb 2016 21:50:35 +0100 Subject: [PATCH 3/8] Option to delete file on record deletion phpcs fix and docs update --- docs/configuration.rst | 4 +++ src/File/Writer/DefaultWriter.php | 17 +++++++++++++ src/File/Writer/WriterInterface.php | 8 ++++++ src/Model/Behavior/UploadBehavior.php | 25 +++++++++++++++++++ .../Model/Behavior/UploadBehaviorTest.php | 9 +++++++ 5 files changed, 63 insertions(+) diff --git a/docs/configuration.rst b/docs/configuration.rst index 588a1849..05ed1297 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -61,3 +61,7 @@ passed in under each field in your behavior configuration. - ``array $settings``: UploadBehavior settings for the current field - Return: (string) the new name for the file + +- ``keepFilesOnDelete``: Keep *all* files when uploading/deleting a record. + + - Default: (boolean) ``true`` diff --git a/src/File/Writer/DefaultWriter.php b/src/File/Writer/DefaultWriter.php index 547f969d..0d8bb550 100644 --- a/src/File/Writer/DefaultWriter.php +++ b/src/File/Writer/DefaultWriter.php @@ -84,6 +84,23 @@ public function write(array $files) return $results; } + /** + * Deletes a set of files to an output + * + * @param array $files the files being written out + * @return array array of results + */ + public function delete(array $files) + { + $filesystem = $this->getFilesystem($this->field, $this->settings); + $results = []; + foreach ($files as $path) { + $results[] = $this->deletePath($filesystem, $path); + } + + return $results; + } + /** * Writes a set of files to an output * diff --git a/src/File/Writer/WriterInterface.php b/src/File/Writer/WriterInterface.php index e44e832c..48ce243d 100644 --- a/src/File/Writer/WriterInterface.php +++ b/src/File/Writer/WriterInterface.php @@ -24,4 +24,12 @@ public function __construct(Table $table, Entity $entity, $data, $field, $settin * @return array array of results */ public function write(array $files); + + /** + * Deletes a set of files to an output + * + * @param array $files the files being written out + * @return array array of results + */ + public function delete(array $files); } diff --git a/src/Model/Behavior/UploadBehavior.php b/src/Model/Behavior/UploadBehavior.php index 9d8c8d78..e79bd95e 100644 --- a/src/Model/Behavior/UploadBehavior.php +++ b/src/Model/Behavior/UploadBehavior.php @@ -95,6 +95,31 @@ public function beforeSave(Event $event, Entity $entity, ArrayObject $options) } } + /** + * Deletes the files after the entity is deleted + * + * @param \Cake\Event\Event $event The afterDelete event that was fired + * @param \Cake\ORM\Entity $entity The entity that was deleted + * @param \ArrayObject $options the options passed to the delete method + * @return void|false + */ + public function afterDelete(Event $event, Entity $entity, ArrayObject $options) + { + foreach ($this->config() as $field => $settings) { + if (Hash::get($settings, 'keepFilesOnDelete', true)) { + continue; + } + + $file = [$entity->{$settings['dir']} . $entity->{$field}]; + $writer = $this->getWriter($entity, [], $field, $settings); + $success = $writer->delete($file); + + if ((new Collection($success))->contains(false)) { + return false; + } + } + } + /** * Retrieves an instance of a path processor which knows how to build paths * for a given file upload diff --git a/tests/TestCase/Model/Behavior/UploadBehaviorTest.php b/tests/TestCase/Model/Behavior/UploadBehaviorTest.php index 8fc2853f..a46beb44 100644 --- a/tests/TestCase/Model/Behavior/UploadBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/UploadBehaviorTest.php @@ -201,6 +201,15 @@ public function testBeforeSaveOk() $this->assertNull($behavior->beforeSave(new Event('fake.event'), $this->entity, new ArrayObject)); } + public function testAfterDelete() + { + $methods = array_diff($this->behaviorMethods, ['config', 'afterDelete']); + $behavior = $this->getMock('Josegonzalez\Upload\Model\Behavior\UploadBehavior', $methods, [$this->table, $this->settings]); + $behavior->config($this->settings); + + $this->assertNull($behavior->afterDelete(new Event('fake.event'), $this->entity, new ArrayObject)); + } + public function testGetWriter() { $processor = $this->behavior->getWriter($this->entity, [], 'field', []); From d5b733395490de1834e933080822a1ebdee07904 Mon Sep 17 00:00:00 2001 From: Joris Vaesen Date: Sat, 27 Feb 2016 19:02:36 +0100 Subject: [PATCH 4/8] more tests --- src/Model/Behavior/UploadBehavior.php | 4 ++- .../File/Writer/DefaultWriterTest.php | 14 +++++++++++ .../Model/Behavior/UploadBehaviorTest.php | 25 ++++++++++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/Model/Behavior/UploadBehavior.php b/src/Model/Behavior/UploadBehavior.php index e79bd95e..f4d13507 100644 --- a/src/Model/Behavior/UploadBehavior.php +++ b/src/Model/Behavior/UploadBehavior.php @@ -110,7 +110,9 @@ public function afterDelete(Event $event, Entity $entity, ArrayObject $options) continue; } - $file = [$entity->{$settings['dir']} . $entity->{$field}]; + $dirField = Hash::get($settings, 'fields.dir', 'dir'); + + $file = [$entity->{$dirField} . $entity->{$field}]; $writer = $this->getWriter($entity, [], $field, $settings); $success = $writer->delete($file); diff --git a/tests/TestCase/File/Writer/DefaultWriterTest.php b/tests/TestCase/File/Writer/DefaultWriterTest.php index a479bd1c..1ca30efa 100644 --- a/tests/TestCase/File/Writer/DefaultWriterTest.php +++ b/tests/TestCase/File/Writer/DefaultWriterTest.php @@ -13,6 +13,8 @@ class DefaultWriterTest extends TestCase { protected $vfs; + protected $writer; + protected $writerMock; public function setup() { @@ -51,6 +53,18 @@ public function testInvoke() ], 'field', [])); } + public function testDelete() + { + $this->assertEquals([], $this->writer->delete([])); + $this->assertEquals([true], $this->writer->delete([ + $this->vfs->path('/tmp/tempfile') + ])); + + $this->assertEquals([false], $this->writer->delete([ + $this->vfs->path('/tmp/invalid.txt') + ])); + } + public function testWriteFile() { $filesystem = $this->getMock('League\Flysystem\FilesystemInterface'); diff --git a/tests/TestCase/Model/Behavior/UploadBehaviorTest.php b/tests/TestCase/Model/Behavior/UploadBehaviorTest.php index a46beb44..6aa94076 100644 --- a/tests/TestCase/Model/Behavior/UploadBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/UploadBehaviorTest.php @@ -201,15 +201,38 @@ public function testBeforeSaveOk() $this->assertNull($behavior->beforeSave(new Event('fake.event'), $this->entity, new ArrayObject)); } - public function testAfterDelete() + public function testAfterDeleteOk() { $methods = array_diff($this->behaviorMethods, ['config', 'afterDelete']); $behavior = $this->getMock('Josegonzalez\Upload\Model\Behavior\UploadBehavior', $methods, [$this->table, $this->settings]); $behavior->config($this->settings); + $behavior->expects($this->any()) + ->method('getWriter') + ->will($this->returnValue($this->writer)); + $this->writer->expects($this->any()) + ->method('delete') + ->will($this->returnValue([true])); + $this->assertNull($behavior->afterDelete(new Event('fake.event'), $this->entity, new ArrayObject)); } + public function testAfterDeleteDeleteFail() + { + $methods = array_diff($this->behaviorMethods, ['config', 'afterDelete']); + $behavior = $this->getMock('Josegonzalez\Upload\Model\Behavior\UploadBehavior', $methods, [$this->table, $this->settings]); + $behavior->config($this->settings); + + $behavior->expects($this->any()) + ->method('getWriter') + ->will($this->returnValue($this->writer)); + $this->writer->expects($this->any()) + ->method('delete') + ->will($this->returnValue([false])); + + $this->assertFalse($behavior->afterDelete(new Event('fake.event'), $this->entity, new ArrayObject)); + } + public function testGetWriter() { $processor = $this->behavior->getWriter($this->entity, [], 'field', []); From 01ea25857b85acc937d8a15ff6cbe60d25d53760 Mon Sep 17 00:00:00 2001 From: Joris Vaesen Date: Sat, 27 Feb 2016 19:21:42 +0100 Subject: [PATCH 5/8] fix afterDelete tests --- .../Model/Behavior/UploadBehaviorTest.php | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/tests/TestCase/Model/Behavior/UploadBehaviorTest.php b/tests/TestCase/Model/Behavior/UploadBehaviorTest.php index 6aa94076..52fdff86 100644 --- a/tests/TestCase/Model/Behavior/UploadBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/UploadBehaviorTest.php @@ -22,6 +22,7 @@ public function setup() 'error' => UPLOAD_ERR_OK, 'size' => 1, 'type' => 'text', + 'keepFilesOnDelete' => false ] ]; $this->dataError = [ @@ -204,8 +205,8 @@ public function testBeforeSaveOk() public function testAfterDeleteOk() { $methods = array_diff($this->behaviorMethods, ['config', 'afterDelete']); - $behavior = $this->getMock('Josegonzalez\Upload\Model\Behavior\UploadBehavior', $methods, [$this->table, $this->settings]); - $behavior->config($this->settings); + $behavior = $this->getMock('Josegonzalez\Upload\Model\Behavior\UploadBehavior', $methods, [$this->table, $this->dataOk]); + $behavior->config($this->dataOk); $behavior->expects($this->any()) ->method('getWriter') @@ -217,11 +218,11 @@ public function testAfterDeleteOk() $this->assertNull($behavior->afterDelete(new Event('fake.event'), $this->entity, new ArrayObject)); } - public function testAfterDeleteDeleteFail() + public function testAfterDeleteFail() { $methods = array_diff($this->behaviorMethods, ['config', 'afterDelete']); - $behavior = $this->getMock('Josegonzalez\Upload\Model\Behavior\UploadBehavior', $methods, [$this->table, $this->settings]); - $behavior->config($this->settings); + $behavior = $this->getMock('Josegonzalez\Upload\Model\Behavior\UploadBehavior', $methods, [$this->table, $this->dataOk]); + $behavior->config($this->dataOk); $behavior->expects($this->any()) ->method('getWriter') @@ -233,6 +234,22 @@ public function testAfterDeleteDeleteFail() $this->assertFalse($behavior->afterDelete(new Event('fake.event'), $this->entity, new ArrayObject)); } + public function testAfterDeleteSkip() + { + $methods = array_diff($this->behaviorMethods, ['config', 'afterDelete']); + $behavior = $this->getMock('Josegonzalez\Upload\Model\Behavior\UploadBehavior', $methods, [$this->table, $this->dataError]); + $behavior->config($this->dataError); + + $behavior->expects($this->any()) + ->method('getWriter') + ->will($this->returnValue($this->writer)); + $this->writer->expects($this->any()) + ->method('delete') + ->will($this->returnValue([true])); + + $this->assertNull($behavior->afterDelete(new Event('fake.event'), $this->entity, new ArrayObject)); + } + public function testGetWriter() { $processor = $this->behavior->getWriter($this->entity, [], 'field', []); From 5a993a14a2fa5b5c3ebc20bdbc20e45de889eaad Mon Sep 17 00:00:00 2001 From: Joris Vaesen Date: Sun, 28 Feb 2016 11:29:59 +0100 Subject: [PATCH 6/8] filename fix --- tests/TestCase/File/Writer/DefaultWriterTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/TestCase/File/Writer/DefaultWriterTest.php b/tests/TestCase/File/Writer/DefaultWriterTest.php index 1ca30efa..feff55ff 100644 --- a/tests/TestCase/File/Writer/DefaultWriterTest.php +++ b/tests/TestCase/File/Writer/DefaultWriterTest.php @@ -13,8 +13,6 @@ class DefaultWriterTest extends TestCase { protected $vfs; - protected $writer; - protected $writerMock; public function setup() { @@ -57,7 +55,7 @@ public function testDelete() { $this->assertEquals([], $this->writer->delete([])); $this->assertEquals([true], $this->writer->delete([ - $this->vfs->path('/tmp/tempfile') + $this->vfs->path('file.txt') ])); $this->assertEquals([false], $this->writer->delete([ From 8da2d611b5be59fc6b8dca6f3a83c8b66a7dbc7b Mon Sep 17 00:00:00 2001 From: Joris Vaesen Date: Thu, 3 Mar 2016 09:24:30 +0100 Subject: [PATCH 7/8] rewrote test using mocks --- src/File/Writer/DefaultWriter.php | 1 + .../File/Writer/DefaultWriterTest.php | 31 ++++++++++++------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/File/Writer/DefaultWriter.php b/src/File/Writer/DefaultWriter.php index 0d8bb550..5efdb3bd 100644 --- a/src/File/Writer/DefaultWriter.php +++ b/src/File/Writer/DefaultWriter.php @@ -143,6 +143,7 @@ public function deletePath(FilesystemInterface $filesystem, $path) } catch (FileNotFoundException $e) { // TODO: log this? } + return $success; } diff --git a/tests/TestCase/File/Writer/DefaultWriterTest.php b/tests/TestCase/File/Writer/DefaultWriterTest.php index feff55ff..bbb79688 100644 --- a/tests/TestCase/File/Writer/DefaultWriterTest.php +++ b/tests/TestCase/File/Writer/DefaultWriterTest.php @@ -13,21 +13,27 @@ class DefaultWriterTest extends TestCase { protected $vfs; + protected $writer; + protected $entity; + protected $table; + protected $data; + protected $field; + protected $settings; public function setup() { - $entity = $this->getMock('Cake\ORM\Entity'); - $table = $this->getMock('Cake\ORM\Table'); - $data = ['tmp_name' => 'path/to/file', 'name' => 'foo.txt']; - $field = 'field'; - $settings = [ + $this->entity = $this->getMock('Cake\ORM\Entity'); + $this->table = $this->getMock('Cake\ORM\Table'); + $this->data = ['tmp_name' => 'path/to/file', 'name' => 'foo.txt']; + $this->field = 'field'; + $this->settings = [ 'filesystem' => [ 'adapter' => function () { return new VfsAdapter(new Vfs); } ] ]; - $this->writer = new DefaultWriter($table, $entity, $data, $field, $settings); + $this->writer = new DefaultWriter($this->table, $this->entity, $this->data, $this->field, $this->settings); $this->vfs = new Vfs; mkdir($this->vfs->path('/tmp')); @@ -53,13 +59,16 @@ public function testInvoke() public function testDelete() { - $this->assertEquals([], $this->writer->delete([])); - $this->assertEquals([true], $this->writer->delete([ - $this->vfs->path('file.txt') + $writer = $this->getMock('Josegonzalez\Upload\File\Writer\DefaultWriter', ['delete'], [$this->table, $this->entity, $this->data, $this->field, $this->settings]); + $writer->expects($this->any())->method('delete')->will($this->returnValue([true])); + $this->assertEquals([true], $writer->delete([ + $this->vfs->path('existing-file.txt') ])); - $this->assertEquals([false], $this->writer->delete([ - $this->vfs->path('/tmp/invalid.txt') + $writer = $this->getMock('Josegonzalez\Upload\File\Writer\DefaultWriter', ['delete'], [$this->table, $this->entity, $this->data, $this->field, $this->settings]); + $writer->expects($this->any())->method('delete')->will($this->returnValue([false])); + $this->assertEquals([false], $writer->delete([ + $this->vfs->path('unexisting-file.txt') ])); } From 8b27ef5aa2c80ef275df3302b8fc0cbadf97654d Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 12 Mar 2016 15:36:49 -0500 Subject: [PATCH 8/8] Mock underlying fileystem writes Rather than mocking the DefaultWriter, we mock the responses from the filesystem to say that a delete has succeeded or failed. This will suffice for testing the proper responses from DefaultWriter::delete() --- .../File/Writer/DefaultWriterTest.php | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/TestCase/File/Writer/DefaultWriterTest.php b/tests/TestCase/File/Writer/DefaultWriterTest.php index bbb79688..aa98951d 100644 --- a/tests/TestCase/File/Writer/DefaultWriterTest.php +++ b/tests/TestCase/File/Writer/DefaultWriterTest.php @@ -33,7 +33,13 @@ public function setup() } ] ]; - $this->writer = new DefaultWriter($this->table, $this->entity, $this->data, $this->field, $this->settings); + $this->writer = new DefaultWriter( + $this->table, + $this->entity, + $this->data, + $this->field, + $this->settings + ); $this->vfs = new Vfs; mkdir($this->vfs->path('/tmp')); @@ -59,16 +65,19 @@ public function testInvoke() public function testDelete() { - $writer = $this->getMock('Josegonzalez\Upload\File\Writer\DefaultWriter', ['delete'], [$this->table, $this->entity, $this->data, $this->field, $this->settings]); - $writer->expects($this->any())->method('delete')->will($this->returnValue([true])); + $filesystem = $this->getMock('League\Flysystem\FilesystemInterface'); + $filesystem->expects($this->at(0))->method('delete')->will($this->returnValue(true)); + $filesystem->expects($this->at(1))->method('delete')->will($this->returnValue(false)); + $writer = $this->getMock('Josegonzalez\Upload\File\Writer\DefaultWriter', ['getFilesystem'], [$this->table, $this->entity, $this->data, $this->field, $this->settings]); + $writer->expects($this->any())->method('getFilesystem')->will($this->returnValue($filesystem)); + + $this->assertEquals([], $writer->delete([])); $this->assertEquals([true], $writer->delete([ - $this->vfs->path('existing-file.txt') + $this->vfs->path('/tmp/tempfile') ])); - $writer = $this->getMock('Josegonzalez\Upload\File\Writer\DefaultWriter', ['delete'], [$this->table, $this->entity, $this->data, $this->field, $this->settings]); - $writer->expects($this->any())->method('delete')->will($this->returnValue([false])); $this->assertEquals([false], $writer->delete([ - $this->vfs->path('unexisting-file.txt') + $this->vfs->path('/tmp/invalid.txt') ])); }