Completed
Push — master ( 739ea2...dd79ae )
by Asmir
13s
created

CacheWarmer::warmUp()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 12
nc 3
nop 1
1
<?php
2
3
/*
4
 * Copyright 2011 Johannes M. Schmitt <[email protected]>
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
namespace JMS\SerializerBundle\Cache;
20
21
use Metadata\MetadataFactoryInterface;
22
use Symfony\Component\Finder\Finder;
23
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
24
25
/**
26
 * @author Asmir Mustafic <[email protected]>
27
 */
28
class CacheWarmer implements CacheWarmerInterface
29
{
30
    /**
31
     * @var MetadataFactoryInterface
32
     */
33
    private $metadataFactory;
34
    /**
35
     * @var string[]
36
     */
37
    private $includePaths = array();
38
    /**
39
     * @var string[]
40
     */
41
    private $excludePaths = array();
42
43
    public function __construct(array $includePaths, MetadataFactoryInterface $metadataFactory, array $excludePaths = array())
44
    {
45
        $this->metadataFactory = $metadataFactory;
46
        $this->includePaths = $includePaths;
47
        $this->excludePaths = $excludePaths;
48
    }
49
50
    public function warmUp($cacheDir)
51
    {
52
        $finder = Finder::create()
53
            ->ignoreVCS(true)
54
            ->ignoreDotFiles(true)
55
            ->ignoreUnreadableDirs(true)
56
            ->in($this->includePaths)
57
            ->exclude($this->excludePaths)
58
            ->name('*.php');
59
60
        foreach ($finder as $file) {
61
            $classes = self::findClasses($file->getPathname());
62
            foreach ($classes as $class) {
63
                $this->metadataFactory->getMetadataForClass($class);
64
            }
65
        }
66
    }
67
68
    public function isOptional()
69
    {
70
        return true;
71
    }
72
73
    /**
74
     * Extract the classes in the given file.
75
     *
76
     * This has been copied from https://github.com/composer/composer/blob/bfed974ae969635e622c4844e5e69526d8459baf/src/Composer/Autoload/ClassMapGenerator.php#L120-L214
77
     *
78
     * @param  string $path The file to check
79
     * @throws \RuntimeException
80
     * @return array             The found classes
81
     */
82
    private static function findClasses($path)
83
    {
84
        $extraTypes = PHP_VERSION_ID < 50400 ? '' : '|trait';
85
        if (defined('HHVM_VERSION') && version_compare(HHVM_VERSION, '3.3', '>=')) {
86
            $extraTypes .= '|enum';
87
        }
88
89
        // Use @ here instead of Silencer to actively suppress 'unhelpful' output
90
        // @link https://github.com/composer/composer/pull/4886
91
        $contents = @php_strip_whitespace($path);
92
        if (!$contents) {
93
            if (!file_exists($path)) {
94
                $message = 'File at "%s" does not exist, check your classmap definitions';
95
            } elseif (!is_readable($path)) {
96
                $message = 'File at "%s" is not readable, check its permissions';
97
            } elseif ('' === trim(file_get_contents($path))) {
98
                // The input file was really empty and thus contains no classes
99
                return array();
100
            } else {
101
                $message = 'File at "%s" could not be parsed as PHP, it may be binary or corrupted';
102
            }
103
            $error = error_get_last();
104
            if (isset($error['message'])) {
105
                $message .= PHP_EOL . 'The following message may be helpful:' . PHP_EOL . $error['message'];
106
            }
107
            throw new \RuntimeException(sprintf($message, $path));
108
        }
109
110
        // return early if there is no chance of matching anything in this file
111
        if (!preg_match('{\b(?:class|interface' . $extraTypes . ')\s}i', $contents)) {
112
            return array();
113
        }
114
115
        // strip heredocs/nowdocs
116
        $contents = preg_replace('{<<<\s*(\'?)(\w+)\\1(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)\\2(?=\r\n|\n|\r|;)}s', 'null', $contents);
117
        // strip strings
118
        $contents = preg_replace('{"[^"\\\\]*+(\\\\.[^"\\\\]*+)*+"|\'[^\'\\\\]*+(\\\\.[^\'\\\\]*+)*+\'}s', 'null', $contents);
119
        // strip leading non-php code if needed
120
        if (substr($contents, 0, 2) !== '<?') {
121
            $contents = preg_replace('{^.+?<\?}s', '<?', $contents, 1, $replacements);
122
            if ($replacements === 0) {
123
                return array();
124
            }
125
        }
126
        // strip non-php blocks in the file
127
        $contents = preg_replace('{\?>.+<\?}s', '?><?', $contents);
128
        // strip trailing non-php code if needed
129
        $pos = strrpos($contents, '?>');
130
        if (false !== $pos && false === strpos(substr($contents, $pos), '<?')) {
131
            $contents = substr($contents, 0, $pos);
132
        }
133
134
        preg_match_all('{
135
            (?:
136
                 \b(?<![\$:>])(?P<type>class|interface' . $extraTypes . ') \s++ (?P<name>[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+)
137
               | \b(?<![\$:>])(?P<ns>namespace) (?P<nsname>\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;]
138
            )
139
        }ix', $contents, $matches);
140
141
        $classes = array();
142
        $namespace = '';
143
144
        for ($i = 0, $len = count($matches['type']); $i < $len; $i++) {
145
            if (!empty($matches['ns'][$i])) {
146
                $namespace = str_replace(array(' ', "\t", "\r", "\n"), '', $matches['nsname'][$i]) . '\\';
147
            } else {
148
                $name = $matches['name'][$i];
149
                // skip anon classes extending/implementing
150
                if ($name === 'extends' || $name === 'implements') {
151
                    continue;
152
                }
153
                if ($name[0] === ':') {
154
                    // This is an XHP class, https://github.com/facebook/xhp
155
                    $name = 'xhp' . substr(str_replace(array('-', ':'), array('_', '__'), $name), 1);
156
                } elseif ($matches['type'][$i] === 'enum') {
157
                    // In Hack, something like:
158
                    //   enum Foo: int { HERP = '123'; }
159
                    // The regex above captures the colon, which isn't part of
160
                    // the class name.
161
                    $name = rtrim($name, ':');
162
                }
163
                $classes[] = ltrim($namespace . $name, '\\');
164
            }
165
        }
166
167
        return $classes;
168
    }
169
}
170