Completed
Push — master ( 0bf176...96018c )
by Beñat
03:06
created

SqlFileRepository::length()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
/*
4
 * This file is part of the BenGorFile package.
5
 *
6
 * (c) Beñat Espiña <[email protected]>
7
 * (c) Gorka Laucirica <[email protected]>
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12
13
namespace BenGorFile\File\Infrastructure\Persistence\Sql;
14
15
use BenGorFile\File\Domain\Model\File;
16
use BenGorFile\File\Domain\Model\FileId;
17
use BenGorFile\File\Domain\Model\FileMimeType;
18
use BenGorFile\File\Domain\Model\FileName;
19
use BenGorFile\File\Domain\Model\FileRepository;
20
use BenGorFile\File\Infrastructure\Domain\Model\FileEventBus;
21
22
/**
23
 * Sql file repository class.
24
 *
25
 * @author Beñat Espiña <[email protected]>
26
 * @author Gorka Laucirica <[email protected]>
27
 * @author Mikel Etxebarria <[email protected]>
28
 */
29
final class SqlFileRepository implements FileRepository
30
{
31
    const DATE_FORMAT = 'Y-m-d H:i:s';
32
33
    /**
34
     * The pdo instance.
35
     *
36
     * @var \PDO
37
     */
38
    private $pdo;
39
40
    /**
41
     * The file event bus, it can be null.
42
     *
43
     * @var FileEventBus|null
44
     */
45
    private $eventBus;
46
47
    /**
48
     * Constructor.
49
     *
50
     * @param \PDO              $aPdo       The pdo instance
51
     * @param FileEventBus|null $anEventBus The file event bus, it can be null
52
     */
53
    public function __construct(\PDO $aPdo, FileEventBus $anEventBus = null)
54
    {
55
        $this->pdo = $aPdo;
56
        $this->eventBus = $anEventBus;
57
    }
58
59
    /**
60
     * {@inheritdoc}
61
     */
62
    public function fileOfId(FileId $anId)
63
    {
64
        $statement = $this->execute('SELECT * FROM file WHERE id = :id', ['id' => $anId->id()]);
65
        if ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
66
            return $this->buildFile($row);
67
        }
68
    }
69
70
    /**
71
     * {@inheritdoc}
72
     */
73 View Code Duplication
    public function query($aSpecification)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
74
    {
75
        $query = $aSpecification->buildQuery();
76
        $preparedStatement = $this->pdo->prepare($query[0]);
77
        foreach ($query[1] as $param => $value) {
78
            if (is_int($value)) {
79
                $preparedStatement->bindValue($param, $value, \PDO::PARAM_INT);
80
            } else {
81
                $preparedStatement->bindValue($param, $value, \PDO::PARAM_STR);
82
            }
83
        }
84
        $preparedStatement->execute();
85
        $rows = $preparedStatement->fetchAll();
86
        if ($rows === null) {
87
            return [];
88
        }
89
90
        return array_map(function ($row) {
91
            return $this->buildFile($row);
92
        }, $rows);
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98 View Code Duplication
    public function singleResultQuery($aSpecification)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
99
    {
100
        $query = $aSpecification->buildQuery();
101
        $preparedStatement = $this->pdo->prepare($query[0]);
102
        foreach ($query[1] as $param => $value) {
103
            if (is_int($value)) {
104
                $preparedStatement->bindValue($param, $value, \PDO::PARAM_INT);
105
            } else {
106
                $preparedStatement->bindValue($param, $value, \PDO::PARAM_STR);
107
            }
108
        }
109
        $preparedStatement->execute();
110
        $row = $preparedStatement->fetch();
111
        if ($row === null) {
112
            return [];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array(); (array) is incompatible with the return type declared by the interface BenGorFile\File\Domain\M...tory::singleResultQuery of type BenGorFile\File\Domain\Model\File.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
113
        }
114
115
        return $this->buildFile($row);
116
    }
117
118
    /**
119
     * {@inheritdoc}
120
     */
121
    public function length($aSpecification)
122
    {
123
        return $this->pdo->query('SELECT COUNT(*) FROM file')->fetchColumn();
124
    }
125
126
    /**
127
     * {@inheritdoc}
128
     */
129 View Code Duplication
    public function fileOfName(FileName $aName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
130
    {
131
        $statement = $this->execute('SELECT * FROM file WHERE name = :name AND extension = :extension', [
132
            'name'      => $aName->name(),
133
            'extension' => $aName->extension(),
134
        ]);
135
        if ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
136
            return $this->buildFile($row);
137
        }
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143 View Code Duplication
    public function all()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
144
    {
145
        $statement = $this->execute('SELECT * FROM file');
0 ignored issues
show
Bug introduced by
The call to execute() misses a required argument $parameters.

This check looks for function calls that miss required arguments.

Loading history...
146
        if ($rows = $statement->fetch(\PDO::FETCH_ASSOC)) {
147
            return array_map(function ($row) {
148
                return $this->buildFile($row);
149
            }, $rows);
150
        }
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     */
156
    public function persist(File $aFile)
157
    {
158
        ($this->exist($aFile)) ? $this->update($aFile) : $this->insert($aFile);
159
160
        if ($this->eventBus instanceof FileEventBus) {
161
            $this->handle($aFile->events());
162
        }
163
    }
164
165
    /**
166
     * {@inheritdoc}
167
     */
168
    public function remove(File $aFile)
169
    {
170
        $this->execute('DELETE FROM file WHERE id = :id', ['id' => $aFile->id()->id()]);
171
172
        if ($this->eventBus instanceof FileEventBus) {
173
            $this->handle($aFile->events());
174
        }
175
    }
176
177
    /**
178
     * Loads the file schema into database create the table
179
     * with file attribute properties as columns.
180
     */
181
    public function initSchema()
182
    {
183
        $this->pdo->exec(<<<'SQL'
184
DROP TABLE IF EXISTS file;
185
CREATE TABLE file (
186
    id CHAR(36) PRIMARY KEY,
187
    name VARCHAR(255) NOT NULL,
188
    extension VARCHAR(100) NOT NULL,
189
    mime_type VARCHAR(255) NOT NULL,
190
    created_on DATETIME NOT NULL,
191
    updated_on DATETIME NOT NULL
192
)
193
SQL
194
        );
195
    }
196
197
    /**
198
     * Checks if the file given exists in the database.
199
     *
200
     * @param File $aFile The file
201
     *
202
     * @return bool
203
     */
204
    private function exist(File $aFile)
205
    {
206
        $count = $this->execute(
207
            'SELECT COUNT(*) FROM file WHERE id = :id', [':id' => $aFile->id()->id()]
208
        )->fetchColumn();
209
210
        return $count === 1;
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $count (string) and 1 (integer) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
211
    }
212
213
    /**
214
     * Prepares the insert SQL with the file given.
215
     *
216
     * @param File $aFile The file
217
     */
218
    private function insert(File $aFile)
219
    {
220
        $sql = 'INSERT INTO file (id, name, extension, mime_type, created_on, updated_on) VALUES (:id, :name, :extension, :mimeType, :createdOn, :updatedOn)';
221
        $this->execute($sql, [
222
            'id'        => $aFile->id()->id(),
223
            'name'      => $aFile->name()->name(),
224
            'extension' => $aFile->name()->extension(),
225
            'mimeType'  => $aFile->mimeType(),
226
            'createdOn' => $aFile->createdOn()->format(self::DATE_FORMAT),
227
            'updatedOn' => $aFile->updatedOn()->format(self::DATE_FORMAT),
228
        ]);
229
    }
230
231
    /**
232
     * Prepares the update SQL with the file given.
233
     *
234
     * @param File $aFile The file
235
     */
236
    private function update(File $aFile)
237
    {
238
        $this->execute('UPDATE file SET name = :name, extension = :extension, mime_type = :mimeType, updated_on = :updatedOn WHERE id = :id', [
239
            'name'      => $aFile->name()->name(),
240
            'extension' => $aFile->name()->extension(),
241
            'mimeType'  => $aFile->mimeType(),
242
            'updatedOn' => $aFile->updatedOn(),
243
            'id'        => $aFile->id()->id(),
244
        ]);
245
    }
246
247
    /**
248
     * Wrapper that encapsulates the same logic about execute the query in PDO.
249
     *
250
     * @param string $aSql       The SQL
251
     * @param array  $parameters Array which contains the parameters of SQL
252
     *
253
     * @return \PDOStatement
254
     */
255
    private function execute($aSql, array $parameters)
256
    {
257
        $statement = $this->pdo->prepare($aSql);
258
        $statement->execute($parameters);
259
260
        return $statement;
261
    }
262
263
    /**
264
     * Builds the file with the given sql row attributes.
265
     *
266
     * @param array $row Array which contains attributes of file
267
     *
268
     * @return File
269
     */
270
    private function buildFile($row)
271
    {
272
        $file = new File(
273
            new FileId($row['id']),
274
            new FileName($row['name'] . '.' . $row['extension']),
275
            new FileMimeType($row['mime_type'])
276
        );
277
278
        $createdOn = new \DateTimeImmutable($row['created_on']);
279
        $updatedOn = new \DateTimeImmutable($row['updated_on']);
280
281
        $file = $this->set($file, 'createdOn', $createdOn);
282
        $file = $this->set($file, 'updatedOn', $updatedOn);
283
284
        return $file;
285
    }
286
287
    /**
288
     * Populates by Reflection the domain object with the given SQL plain values.
289
     *
290
     * @param File   $file          The file domain object
291
     * @param string $propertyName  The property name
292
     * @param mixed  $propertyValue The property value
293
     *
294
     * @return File
295
     */
296
    private function set(File $file, $propertyName, $propertyValue)
297
    {
298
        $reflectionFile = new \ReflectionClass($file);
299
        $reflectionCreatedOn = $reflectionFile->getProperty($propertyName);
300
        $reflectionCreatedOn->setAccessible(true);
301
        $reflectionCreatedOn->setValue($file, $propertyValue);
302
303
        return $file;
304
    }
305
306
    /**
307
     * Handles the given events with event bus.
308
     *
309
     * @param array $events A collection of file domain events
310
     */
311
    private function handle($events)
312
    {
313
        foreach ($events as $event) {
314
            $this->eventBus->handle($event);
315
        }
316
    }
317
}
318