Issues (15)

src/FileRepository.php (1 issue)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cycle\Migrations;
6
7
use DateTimeInterface;
8
use Doctrine\Inflector\Inflector;
9
use Doctrine\Inflector\Rules\English\InflectorFactory;
10
use Spiral\Core\Container;
11
use Spiral\Core\FactoryInterface;
12
use Spiral\Files\Files;
13
use Spiral\Files\FilesInterface;
14
use Cycle\Migrations\Config\MigrationConfig;
15
use Cycle\Migrations\Exception\RepositoryException;
16
use Spiral\Tokenizer\Reflection\ReflectionFile;
17
18
/**
19
 * Stores migrations as files.
20
 *
21
 * @psalm-type TFileArray = array{
22
 *     filename: non-empty-string,
23
 *     class: class-string,
24
 *     created: DateTimeInterface|null,
25
 *     chunk: string,
26
 *     name: non-empty-string
27
 * }
28
 */
29
final class FileRepository implements RepositoryInterface
30
{
31
    // Migrations file name format. This format will be used when requesting new migration filename.
32 584
    private const FILENAME_FORMAT = '%s_%s_%s.php';
33
34 584
    // Timestamp format for files.
35 584
    private const TIMESTAMP_FORMAT = 'Ymd.His';
36 584
37
    private int $chunkID = 0;
38
    private FactoryInterface $factory;
39
    private FilesInterface $files;
40
    private Inflector $inflector;
41
42 344
    public function __construct(private MigrationConfig $config, FactoryInterface $factory = null)
43
    {
44 344
        $this->files = new Files();
45 344
        $this->factory = $factory ?? new Container();
46 344
        $this->inflector = (new InflectorFactory())->build();
47
    }
48 344
49 320
    public function getMigrations(): array
50
    {
51 146
        $timestamps = [];
52
        $chunks = [];
53
        $migrations = [];
54
55 320
        foreach ($this->getFilesIterator() as $f) {
56
            if (! \class_exists($f['class'], false)) {
57 320
                //Attempting to load migration class (we can not relay on autoloading here)
58 320
                require_once($f['filename']);
59 320
            }
60
61
            /** @var MigrationInterface $migration */
62 328
            $migration = $this->factory->make($f['class']);
63
64 328
            $timestamps[] = $f['created']->getTimestamp();
65
            $chunks[] = $f['chunk'];
66
            $migrations[] = $migration->withState(new State($f['name'], $f['created']));
67
        }
68
69
        \array_multisort($timestamps, $chunks, SORT_NATURAL, $migrations);
0 ignored issues
show
Cycle\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

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