Passed
Push — master ( 10a192...087136 )
by Aleksei
07:38 queued 05:42
created

FileRepository::getMigrations()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 23
c 0
b 0
f 0
rs 9.8666
cc 3
nc 3
nop 0
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license MIT
7
 * @author  Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Migrations;
13
14
use Spiral\Core\Container;
15
use Spiral\Core\FactoryInterface;
16
use Spiral\Files\Files;
17
use Spiral\Files\FilesInterface;
18
use Cycle\Migrations\Config\MigrationConfig;
19
use Cycle\Migrations\Exception\RepositoryException;
20
use Spiral\Tokenizer\Reflection\ReflectionFile;
21
22
/**
23
 * Stores migrations as files.
24
 */
25
final class FileRepository implements RepositoryInterface
26
{
27
    // Migrations file name format. This format will be used when requesting new migration filename.
28
    private const FILENAME_FORMAT = '%s_%s_%s.php';
29
30
    // Timestamp format for files.
31
    private const TIMESTAMP_FORMAT = 'Ymd.His';
32
33
    /** @var MigrationConfig */
34
    private $config;
35
36
    /** @var int */
37
    private $chunkID = 0;
38
39
    /** @var FactoryInterface */
40
    private $factory;
41
42
    /** @var FilesInterface */
43
    private $files;
44
45
    /** @var \Doctrine\Inflector\Inflector */
46
    private $inflector;
47
48
    /**
49
     * @param MigrationConfig       $config
50
     * @param FactoryInterface|null $factory
51
     */
52
    public function __construct(MigrationConfig $config, FactoryInterface $factory = null)
53
    {
54
        $this->config = $config;
55
        $this->files = new Files();
56
        $this->factory = $factory ?? new Container();
57
        $this->inflector = (new \Doctrine\Inflector\Rules\English\InflectorFactory())->build();
58
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63
    public function getMigrations(): array
64
    {
65
        $timestamps = [];
66
        $chunks = [];
67
        $migrations = [];
68
69
        foreach ($this->getFiles() as $f) {
70
            if (!class_exists($f['class'], false)) {
71
                //Attempting to load migration class (we can not relay on autoloading here)
72
                require_once($f['filename']);
73
            }
74
75
            /** @var MigrationInterface $migration */
76
            $migration = $this->factory->make($f['class']);
77
78
            $timestamps[] = $f['created']->getTimestamp();
79
            $chunks[] = $f['chunk'];
80
            $migrations[] = $migration->withState(new State($f['name'], $f['created']));
81
        }
82
83
        array_multisort($timestamps, $chunks, SORT_ASC | SORT_NATURAL, $migrations);
0 ignored issues
show
Bug introduced by
Cycle\Migrations\SORT_AS...Migrations\SORT_NATURAL cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

83
        array_multisort($timestamps, $chunks, /** @scrutinizer ignore-type */ SORT_ASC | SORT_NATURAL, $migrations);
Loading history...
84
85
        return $migrations;
86
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91
    public function registerMigration(string $name, string $class, string $body = null): string
92
    {
93
        if (empty($body) && !class_exists($class)) {
94
            throw new RepositoryException(
95
                "Unable to register migration '{$class}', representing class does not exists"
96
            );
97
        }
98
99
        $currentTimeStamp = date(self::TIMESTAMP_FORMAT);
100
        $inflectedName = $this->inflector->tableize($name);
101
102
        foreach ($this->getMigrations() as $migration) {
103
            if (get_class($migration) === $class) {
104
                throw new RepositoryException(
105
                    "Unable to register migration '{$class}', migration already exists"
106
                );
107
            }
108
109
            if (
110
                $migration->getState()->getName() === $inflectedName
111
                && $migration->getState()->getTimeCreated()->format(self::TIMESTAMP_FORMAT) === $currentTimeStamp
112
            ) {
113
                throw new RepositoryException(
114
                    "Unable to register migration '{$inflectedName}', migration under the same name already exists"
115
                );
116
            }
117
        }
118
119
        if (empty($body)) {
120
            //Let's read body from a given class filename
121
            $body = $this->files->read((new \ReflectionClass($class))->getFileName());
122
        }
123
124
        $filename = $this->createFilename($name);
125
126
        //Copying
127
        $this->files->write($filename, $body, FilesInterface::READONLY, true);
128
129
        return $filename;
130
    }
131
132
    /**
133
     * Internal method to fetch all migration filenames.
134
     */
135
    private function getFiles(): \Generator
136
    {
137
        foreach ($this->files->getFiles($this->config->getDirectory(), '*.php') as $filename) {
138
            $reflection = new ReflectionFile($filename);
139
            $definition = explode('_', basename($filename));
140
141
            if (count($definition) < 3) {
142
                throw new RepositoryException("Invalid migration filename '{$filename}'");
143
            }
144
145
            $created = \DateTime::createFromFormat(self::TIMESTAMP_FORMAT, $definition[0]);
146
            if (false === $created) {
147
                throw new RepositoryException("Invalid migration filename '{$filename}' - corrupted date format");
148
            }
149
150
            yield [
151
                'filename' => $filename,
152
                'class' => $reflection->getClasses()[0],
153
                'created' => $created,
154
                'chunk' => $definition[1],
155
                'name' => str_replace(
156
                    '.php',
157
                    '',
158
                    implode('_', array_slice($definition, 2))
159
                ),
160
            ];
161
        }
162
    }
163
164
    /**
165
     * Request new migration filename based on user input and current timestamp.
166
     *
167
     * @param string $name
168
     *
169
     * @return string
170
     */
171
    private function createFilename(string $name): string
172
    {
173
        $name = $this->inflector->tableize($name);
174
175
        $filename = sprintf(
176
            self::FILENAME_FORMAT,
177
            date(self::TIMESTAMP_FORMAT),
178
            $this->chunkID++,
179
            $name
180
        );
181
182
        return $this->files->normalizePath(
183
            $this->config->getDirectory() . FilesInterface::SEPARATOR . $filename
184
        );
185
    }
186
}
187