• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Cecilapp / Cecil / 27485383570

14 Jun 2026 02:01AM UTC coverage: 81.528%. Remained the same
27485383570

push

github

ArnaudLigny
fix: adjust PHAR composer path and Box config

Include composer files and icon in the Box build and refine Finder rules. Added "exclude-composer-files": false and a "files" list (composer.json, resources/icon.png), updated finder to include README.md and to treat composer.json/lock appropriately. Fix Util::resolveComposerFilePath() to use Platform::getPharPath() directly when composing the composer.json path (remove redundant phar:// prefix). These changes ensure composer metadata is packaged correctly and path resolution works inside the PHAR.

0 of 1 new or added line in 1 file covered. (0.0%)

3522 of 4320 relevant lines covered (81.53%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.37
/src/Util.php
1
<?php
2

3
/**
4
 * This file is part of Cecil.
5
 *
6
 * (c) Arnaud Ligny <arnaud@ligny.fr>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
declare(strict_types=1);
13

14
namespace Cecil;
15

16
use Cecil\Util\Platform;
17
use Symfony\Component\Filesystem\Path;
18

19
/**
20
 * Utility class.
21
 *
22
 * Provides various utility methods for formatting class names, method names,
23
 * joining paths, converting memory sizes, and more.
24
 */
25
class Util
26
{
27
    /**
28
     * Formats a class name.
29
     *
30
     * ie: "Cecil\Step\OptimizeHtml" become "OptimizeHtml"
31
     *
32
     * @param object $class
33
     */
34
    public static function formatClassName($class, array $options = []): string
35
    {
36
        $lowercase = false;
1✔
37
        extract($options, EXTR_IF_EXISTS);
1✔
38

39
        $className = substr(strrchr(\get_class($class), '\\'), 1);
1✔
40
        if ($lowercase) {
1✔
41
            $className = strtolower($className);
1✔
42
        }
43

44
        return $className;
1✔
45
    }
46

47
    /**
48
     * Formats a method name.
49
     *
50
     * ie: "Cecil\Renderer\Extension\Core::asset()" become "asset()"
51
     *
52
     * @param string $method
53
     */
54
    public static function formatMethodName(string $method): string
55
    {
56
        $methodName = explode('::', $method)[1];
×
57

58
        return $methodName;
×
59
    }
60

61
    /**
62
     * Converts an array of strings into a path.
63
     */
64
    public static function joinPath(string ...$path): string
65
    {
66
        $path = array_filter($path, function ($path) {
1✔
67
            return !empty($path) && !\is_null($path);
1✔
68
        });
1✔
69
        array_walk($path, function (&$value, $key) {
1✔
70
            $value = str_replace('\\', '/', $value);
1✔
71
            $value = rtrim($value, '/');
1✔
72
            $value = $key == 0 ? $value : ltrim($value, '/');
1✔
73
        });
1✔
74

75
        return Path::canonicalize(implode('/', $path));
1✔
76
    }
77

78
    /**
79
     * Converts an array of strings into a system path.
80
     */
81
    public static function joinFile(string ...$path): string
82
    {
83
        array_walk($path, function (&$value, $key) use (&$path) {
2✔
84
            $value = str_replace(['\\', '/'], [DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR], $value);
2✔
85
            $value = rtrim($value, DIRECTORY_SEPARATOR);
2✔
86
            $value = $key == 0 ? $value : ltrim($value, DIRECTORY_SEPARATOR);
2✔
87
            // unset entry with empty value
88
            if (empty($value)) {
2✔
89
                unset($path[$key]);
1✔
90
            }
91
        });
2✔
92

93
        return implode(DIRECTORY_SEPARATOR, $path);
2✔
94
    }
95

96
    /**
97
     * Converts memory size for human.
98
     */
99
    public static function convertMemory($size): string
100
    {
101
        if ($size === 0) {
1✔
102
            return '0';
×
103
        }
104
        $unit = ['b', 'kb', 'mb', 'gb', 'tb', 'pb'];
1✔
105

106
        return \sprintf('%s %s', round($size / pow(1024, $i = floor(log($size, 1024))), 2), $unit[$i]);
1✔
107
    }
108

109
    /**
110
     * Converts a duration (in seconds) to a human-readable string.
111
     */
112
    public static function convertDuration(float $duration): string
113
    {
114
        if ($duration < 1) {
1✔
115
            return \sprintf('%s ms', round($duration * 1000, 0));
1✔
116
        }
117

118
        return \sprintf('%s s', round($duration, 2));
1✔
119
    }
120

121
    /**
122
     * Converts microtime interval for human.
123
     */
124
    public static function convertMicrotime(float $start): string
125
    {
126
        return self::convertDuration(microtime(true) - $start);
×
127
    }
128

129
    /**
130
     * Extracts PHP minimum version and required extensions from composer.json.
131
     *
132
     * @return array{minimumVersion: string, requiredExtensions: array<int, string>}
133
     */
134
    public static function getPhpRequirements(): array
135
    {
136
        $composerFile = self::resolveComposerFilePath();
×
137
        $composerRaw = file_get_contents($composerFile);
×
138
        if ($composerRaw === false) {
×
139
            throw new \RuntimeException(\sprintf('Unable to read `%s`.', $composerFile));
×
140
        }
141

142
        $composer = json_decode($composerRaw, true);
×
143
        if (!\is_array($composer) || !isset($composer['require']) || !\is_array($composer['require'])) {
×
144
            throw new \RuntimeException(\sprintf('Unable to parse required packages from `%s`.', $composerFile));
×
145
        }
146

147
        $phpVersionConstraint = $composer['require']['php'] ?? null;
×
148
        if (!\is_string($phpVersionConstraint) || !preg_match('/(\d+\.\d+(?:\.\d+)?)/', $phpVersionConstraint, $matches)) {
×
149
            throw new \RuntimeException(\sprintf('Unable to parse PHP version requirement from `%s`.', $composerFile));
×
150
        }
151

152
        $phpMinimumVersion = $matches[1];
×
153
        if (substr_count($phpMinimumVersion, '.') === 1) {
×
154
            $phpMinimumVersion .= '.0';
×
155
        }
156

157
        $phpRequiredExtensions = [];
×
158
        foreach (array_keys($composer['require']) as $requirement) {
×
159
            if (str_starts_with($requirement, 'ext-')) {
×
160
                $phpRequiredExtensions[] = substr($requirement, 4);
×
161
            }
162
        }
163
        sort($phpRequiredExtensions);
×
164

165
        return [
×
166
            'minimumVersion' => $phpMinimumVersion,
×
167
            'requiredExtensions' => $phpRequiredExtensions,
×
168
        ];
×
169
    }
170

171
    /**
172
     * Resolve composer.json path from Cecil root (including PHAR context).
173
     */
174
    private static function resolveComposerFilePath(): string
175
    {
176
        if (Platform::isPhar()) {
×
NEW
177
            return \sprintf('%s/composer.json', Platform::getPharPath());
×
178
        }
179

180
        return self::joinFile(__DIR__, '/../composer.json');
×
181
    }
182

183
    /**
184
     * Loads class from the source directory, in the given subdirectory $dir.
185
     */
186
    public static function autoload(Builder $builder, string $dir): void
187
    {
188
        spl_autoload_register(function ($className) use ($builder, $dir) {
1✔
189
            $classFile = Util::joinFile($builder->getConfig()->getSourceDir(), $dir, "$className.php");
1✔
190
            if (is_readable($classFile)) {
1✔
191
                require $classFile;
1✔
192
                return;
1✔
193
            }
194
            // in themes
195
            foreach ($builder->getConfig()->getTheme() ?? [] as $theme) {
1✔
196
                $classFile = Util::joinFile($builder->getConfig()->getThemeDirPath($theme, $dir), "$className.php");
1✔
197
                if (is_readable($classFile)) {
1✔
198
                    require $classFile;
×
199
                    return;
×
200
                }
201
            }
202
        });
1✔
203
    }
204

205
    /**
206
     * Matches a URL against known embedded content patterns.
207
     * Supports YouTube, Vimeo, Dailymotion, and GitHub Gists.
208
     *
209
     * @param string $url The URL to check
210
     *
211
     * @return array|false An associative array with 'type' and 'url' keys if a match is found, or false otherwise
212
     */
213
    public static function matchesUrlPattern(string $url): array|false
214
    {
215
        $services = [
1✔
216
            'youtube' => [
1✔
217
                // https://regex101.com/r/gznM1j/1
218
                'pattern' => '(?:https?:\/\/)?(?:www\.)?youtu(?:\.be\/|be.com\/\S*(?:watch|embed)(?:(?:(?=\/[-a-zA-Z0-9_]{11,}(?!\S))\/)|(?:\S*v=|v\/)))([-a-zA-Z0-9_]{11,})',
1✔
219
                'baseurl' => 'https://www.youtube-nocookie.com/embed/',
1✔
220
                'type' => 'video',
1✔
221
            ],
1✔
222
            'vimeo' => [
1✔
223
                // https://regex101.com/r/wCEFhd/1
224
                'pattern' => 'https:\/\/vimeo\.com\/([0-9]+)',
1✔
225
                'baseurl' => 'https://player.vimeo.com/video/',
1✔
226
                'type' => 'video',
1✔
227
            ],
1✔
228
            'dailymotion' => [
1✔
229
                // https://regex101.com/r/YKnLPm/1
230
                'pattern' => '(?:https?:\/\/)?(?:www\.)?dailymotion\.com\/video\/([a-z0-9]+)',
1✔
231
                'baseurl' => 'https://geo.dailymotion.com/player.html?video=',
1✔
232
                'type' => 'video',
1✔
233
            ],
1✔
234
            'github_gist' => [
1✔
235
                // https://regex101.com/r/y3bm2M/1
236
                'pattern' => 'https:\/\/gist\.github\.com\/([-a-zA-Z0-9_]+\/[-a-zA-Z0-9_]+)',
1✔
237
                'baseurl' => 'https://gist.github.com/',
1✔
238
                'type' => 'script',
1✔
239
            ],
1✔
240
        ];
1✔
241

242
        foreach ($services as $service) {
1✔
243
            if (preg_match('/' . $service['pattern'] . '/is', $url, $matches)) {
1✔
244
                return [
1✔
245
                    'type' => $service['type'],
1✔
246
                    'url' => $service['baseurl'] . $matches[1],
1✔
247
                ];
1✔
248
            }
249
        }
250

251
        return false;
1✔
252
    }
253
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc