Completed
Push — 2.0 ( 1c5343...db1655 )
by Christopher
05:04
created

functions.php ➔ normalizePath()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 9
nop 2
dl 0
loc 17
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * Licensed under The GPL-3.0 License
4
 * For full copyright and license information, please see the LICENSE.txt
5
 * Redistributions of files must retain the above copyright notice.
6
 *
7
 * @since    2.0.0
8
 * @author   Christopher Castro <[email protected]>
9
 * @link     http://www.quickappscms.org
10
 * @license  http://opensource.org/licenses/gpl-3.0.html GPL-3.0 License
11
 */
12
13
use Cake\Cache\Cache;
14
use Cake\Core\Configure;
15
use Cake\Datasource\ConnectionManager;
16
use Cake\Error\Debugger;
17
use Cake\Error\FatalErrorException;
18
use Cake\Event\EventManager;
19
use Cake\Filesystem\File;
20
use Cake\Filesystem\Folder;
21
use Cake\I18n\I18n;
22
use Cake\ORM\Entity;
23
use Cake\ORM\TableRegistry;
24
use Cake\Routing\Router;
25
use Cake\Utility\Inflector;
26
use CMS\Core\Plugin;
27
28
if (!function_exists('snapshot')) {
29
    /**
30
     * Stores some bootstrap-handy information into a persistent file.
31
     *
32
     * Information is stored in `TMP/snapshot.php` file, it contains
33
     * useful information such as enabled languages, content types slugs, installed
34
     * plugins, etc.
35
     *
36
     * You can read this information using `Configure::read()` as follow:
37
     *
38
     * ```php
39
     * Configure::read('QuickApps.<option>');
40
     * ```
41
     *
42
     * Or using the `quickapps()` global function:
43
     *
44
     * ```php
45
     * quickapps('<option>');
46
     * ```
47
     *
48
     * @return void
49
     */
50
    function snapshot()
51
    {
52
        if (Cache::config('default')) {
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Core\StaticConfigTrait::config() has been deprecated with message: 3.4.0 Use setConfig()/getConfig() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
53
            Cache::clear(false, 'default');
54
        }
55
56
        if (Cache::config('_cake_core_')) {
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Core\StaticConfigTrait::config() has been deprecated with message: 3.4.0 Use setConfig()/getConfig() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
57
            Cache::clear(false, '_cake_core_');
58
        }
59
60
        if (Cache::config('_cake_model_')) {
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Core\StaticConfigTrait::config() has been deprecated with message: 3.4.0 Use setConfig()/getConfig() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
61
            Cache::clear(false, '_cake_model_');
62
        }
63
64
        $versionPath = QUICKAPPS_CORE . 'VERSION.txt';
65
        $snapshot = [
66
            'version' => null,
67
            'content_types' => [],
68
            'plugins' => [],
69
            'options' => [],
70
            'languages' => [],
71
            'aspects' => [],
72
        ];
73
74
        if (is_readable($versionPath)) {
75
            $versionFile = file($versionPath);
76
            $snapshot['version'] = trim(array_pop($versionFile));
77
        } else {
78
            die(sprintf('Missing file: %s', $versionPath));
79
        }
80
81
        if (ConnectionManager::config('default')) {
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Core\StaticConfigTrait::config() has been deprecated with message: 3.4.0 Use setConfig()/getConfig() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
82
            if (!TableRegistry::exists('SnapshotPlugins')) {
83
                $PluginTable = TableRegistry::get('SnapshotPlugins', ['table' => 'plugins']);
84
            } else {
85
                $PluginTable = TableRegistry::get('SnapshotPlugins');
86
            }
87
88
            if (!TableRegistry::exists('SnapshotContentTypes')) {
89
                $ContentTypesTable = TableRegistry::get('SnapshotContentTypes', ['table' => 'content_types']);
90
            } else {
91
                $ContentTypesTable = TableRegistry::get('SnapshotContentTypes');
92
            }
93
94
            if (!TableRegistry::exists('SnapshotLanguages')) {
95
                $LanguagesTable = TableRegistry::get('SnapshotLanguages', ['table' => 'languages']);
96
            } else {
97
                $LanguagesTable = TableRegistry::get('SnapshotLanguages');
98
            }
99
100
            if (!TableRegistry::exists('SnapshotOptions')) {
101
                $OptionsTable = TableRegistry::get('SnapshotOptions', ['table' => 'options']);
102
            } else {
103
                $OptionsTable = TableRegistry::get('SnapshotOptions');
104
            }
105
106
            $PluginTable->schema(['value' => 'serialized']);
0 ignored issues
show
Deprecated Code introduced by
The method Cake\ORM\Table::schema() has been deprecated with message: 3.4.0 Use setSchema()/getSchema() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
107
            $OptionsTable->schema(['value' => 'serialized']);
0 ignored issues
show
Deprecated Code introduced by
The method Cake\ORM\Table::schema() has been deprecated with message: 3.4.0 Use setSchema()/getSchema() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
108
109
            $plugins = $PluginTable->find()
110
                ->select(['name', 'package', 'status'])
111
                ->order([
112
                    'ordering' => 'ASC',
113
                    'name' => 'ASC',
114
                ])
115
                ->all();
116
            $contentTypes = $ContentTypesTable->find()
117
                ->select(['slug'])
118
                ->all();
119
            $languages = $LanguagesTable->find()
120
                ->where(['status' => 1])
121
                ->order(['ordering' => 'ASC'])
122
                ->all();
123
            $options = $OptionsTable->find()
124
                ->select(['name', 'value'])
125
                ->where(['autoload' => 1])
126
                ->all();
127
128
            foreach ($contentTypes as $contentType) {
129
                $snapshot['content_types'][] = $contentType->slug;
130
            }
131
132
            foreach ($options as $option) {
133
                $snapshot['options'][$option->name] = $option->value;
134
            }
135
136
            foreach ($languages as $language) {
137
                list($languageCode, $countryCode) = localeSplit($language->code);
138
                $snapshot['languages'][$language->code] = [
139
                    'name' => $language->name,
140
                    'locale' => $language->code,
141
                    'code' => $languageCode,
142
                    'country' => $countryCode,
143
                    'direction' => $language->direction,
144
                    'icon' => $language->icon,
145
                ];
146
            }
147
        } else {
148
            $plugins = [];
149
            foreach (Plugin::scan() as $plugin => $path) {
150
                $plugins[] = new Entity([
151
                    'name' => $plugin,
152
                    'status' => true,
153
                    'package' => 'quickapps-plugins',
154
                ]);
155
            }
156
        }
157
158
        $folder = new Folder(QUICKAPPS_CORE . 'src/Aspect/');
159
        foreach ($folder->read(false, false, true)[1] as $classFile) {
160
            $className = basename(preg_replace('/\.php$/', '', $classFile));
161
            if (!in_array($className, ['AppAspect', 'Aspect'])) {
162
                $snapshot['aspects'][] = "CMS\\Aspect\\{$className}";
163
            }
164
        }
165
166
        foreach ($plugins as $plugin) {
167
            $pluginPath = false;
168
169
            if (isset(Plugin::scan()[$plugin->name])) {
170
                $pluginPath = Plugin::scan()[$plugin->name];
171
            }
172
173
            if ($pluginPath === false) {
174
                Debugger::log(sprintf('Plugin "%s" was found in DB but QuickAppsCMS was unable to locate its root directory.', $plugin->name));
175
                continue;
176
            }
177
178
            if (!Plugin::validateJson("{$pluginPath}/composer.json")) {
179
                Debugger::log(sprintf('Plugin "%s" has a corrupt "composer.json" file (%s).', $plugin->name, "{$pluginPath}/composer.json"));
180
                continue;
181
            }
182
183
            $aspectsPath = "{$pluginPath}/src/Aspect/";
184
            $eventsPath = "{$pluginPath}/src/Event/";
185
            $fieldsPath = "{$pluginPath}/src/Field/";
186
            $helpFiles = glob($pluginPath . '/src/Template/Element/Help/help*.ctp');
187
            $isTheme = str_ends_with($plugin->name, 'Theme');
188
            $status = (bool)$plugin->status;
189
            $humanName = '';
190
            $aspects = [];
191
            $eventListeners = [];
192
            $fields = [];
193
194
            $subspaces = [
195
                $aspectsPath => 'Aspect',
196
                $eventsPath => 'Event',
197
                $fieldsPath => 'Field',
198
            ];
199
            $varnames = [
200
                $aspectsPath => 'aspects',
201
                $eventsPath => 'eventListeners',
202
                $fieldsPath => 'fields',
203
            ];
204
            foreach ([$aspectsPath, $eventsPath, $fieldsPath] as $path) {
205
                if (is_dir($path)) {
206
                    $Folder = new Folder($path);
207
                    foreach ($Folder->read(false, false, true)[1] as $classFile) {
208
                        $className = basename(preg_replace('/\.php$/', '', $classFile));
209
                        $subspace = $subspaces[$path];
210
                        $varname = $varnames[$path];
211
                        $namespace = "{$plugin->name}\\{$subspace}\\";
212
                        ${$varname}[] = $namespace . $className;
213
                    }
214
                }
215
            }
216
217
            if (is_readable("{$pluginPath}composer.json")) {
218
                $json = (array)json_decode(file_get_contents("{$pluginPath}composer.json"), true);
219
                if (!empty($json['extra']['human-name'])) {
220
                    $humanName = $json['extra']['human-name'];
221
                }
222
            }
223
224
            if (empty($humanName)) {
225
                $humanName = (string)Inflector::humanize((string)Inflector::underscore($plugin->name));
226
                if ($isTheme) {
227
                    $humanName = trim(str_replace_last('Theme', '', $humanName));
228
                }
229
            }
230
231
            $snapshot['plugins'][$plugin->name] = [
232
                'name' => $plugin->name,
233
                'humanName' => $humanName,
234
                'package' => $plugin->package,
235
                'isTheme' => $isTheme,
236
                'hasHelp' => !empty($helpFiles),
237
                'hasSettings' => is_readable($pluginPath . '/src/Template/Element/settings.ctp'),
238
                'aspects' => $aspects,
239
                'eventListeners' => $eventListeners,
240
                'fields' => $fields,
241
                'status' => $status,
242
                'path' => $pluginPath,
243
            ];
244
245
            if ($status) {
246
                $snapshot['aspects'] = array_merge($snapshot['aspects'], $aspects);
247
            }
248
        }
249
250
        Configure::write('QuickApps', $snapshot);
251
        if (!Configure::dump('snapshot', 'QuickApps', ['QuickApps'])) {
252
            die('QuickAppsCMS was unable to create a snapshot file, check that PHP have permission to write to the "/tmp" directory.');
253
        }
254
    }
255
}
256
257
if (!function_exists('normalizePath')) {
258
    /**
259
     * Normalizes the given file system path, makes sure that all DIRECTORY_SEPARATOR
260
     * are the same according to current OS, so you won't get a mix of "/" and "\" in
261
     * your paths.
262
     *
263
     * ### Example:
264
     *
265
     * ```php
266
     * normalizePath('/path\to/filename\with\backslash.zip');
267
     * // output LINUX: /path/to/filename\with\backslashes.zip
268
     * // output WINDOWS: /path/to/filename/with/backslashes.zip
269
     * ```
270
     *
271
     * You can indicate which "directory separator" symbol to use using the second
272
     * argument:
273
     *
274
     * ```php
275
     * normalizePath('/path\to/filename\with\backslash.zip', '\');
276
     * // output LINUX & WIDNOWS: \path\to\filename\with\backslash.zip
277
     * ```
278
     *
279
     * By defaults uses DIRECTORY_SEPARATOR as symbol.
280
     *
281
     * @param string $path The path to normalize
282
     * @param string $ds Directory separator character, defaults to DIRECTORY_SEPARATOR
283
     * @return string Normalized $path
284
     */
285
    function normalizePath($path, $ds = DIRECTORY_SEPARATOR)
286
    {
287
        $tail = '';
288
        $base = $path;
289
290
        if (DIRECTORY_SEPARATOR === '/') {
291
            $lastDS = strrpos($path, $ds);
292
            $tail = $lastDS !== false && $lastDS !== strlen($path) - 1 ? substr($path, $lastDS + 1) : '';
293
            $base = $tail ? substr($path, 0, $lastDS + 1) : $path;
294
        }
295
296
        $path = str_replace(['/', '\\', "{$ds}{$ds}"], $ds, $base);
297
        $path = str_replace("{$ds}{$ds}", $ds, $path);
298
        $path .= $tail;
299
300
        return $path;
301
    }
302
}
303
304
if (!function_exists('quickapps')) {
305
    /**
306
     * Shortcut for reading QuickApps's snapshot configuration.
307
     *
308
     * For example, `quickapps('variables');` maps to
309
     * `Configure::read('QuickApps.variables');`. If this function is used with
310
     * no arguments, `quickapps()`, the entire snapshot will be returned.
311
     *
312
     * @param string $key The key to read from snapshot, or null to read the whole
313
     *  snapshot's info
314
     * @return mixed
315
     */
316
    function quickapps($key = null)
317
    {
318
        if ($key !== null) {
319
            return Configure::read("QuickApps.{$key}");
320
        }
321
322
        return Configure::read('QuickApps');
323
    }
324
}
325
326
if (!function_exists('option')) {
327
    /**
328
     * Shortcut for getting an option value from "options" DB table.
329
     *
330
     * The second arguments, $default,  is used as default value to return if no
331
     * value is found. If not value is found and not default values was given this
332
     * function will return `false`.
333
     *
334
     * ### Example:
335
     *
336
     * ```php
337
     * option('site_slogan');
338
     * ```
339
     *
340
     * @param string $name Name of the option to retrieve. e.g. `front_theme`,
341
     *  `default_language`, `site_slogan`, etc
342
     * @param mixed $default The default value to return if no value is found
343
     * @return mixed Current value for the specified option. If the specified option
344
     *  does not exist, returns boolean FALSE
345
     */
346
    function option($name, $default = false)
347
    {
348
        if (Configure::check("QuickApps.options.{$name}")) {
349
            return Configure::read("QuickApps.options.{$name}");
350
        }
351
352
        if (ConnectionManager::config('default')) {
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Core\StaticConfigTrait::config() has been deprecated with message: 3.4.0 Use setConfig()/getConfig() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
353
            $option = TableRegistry::get('Options')
354
                ->find()
355
                ->where(['Options.name' => $name])
356
                ->first();
357
            if ($option) {
358
                return $option->value;
359
            }
360
        }
361
362
        return $default;
363
    }
364
}
365
366
if (!function_exists('plugin')) {
367
    /**
368
     * Shortcut for "Plugin::get()".
369
     *
370
     * ### Example:
371
     *
372
     * ```php
373
     * $specialSetting = plugin('MyPlugin')->settings['special_setting'];
374
     * ```
375
     *
376
     * @param string $plugin Plugin name to get, or null to get a collection of
377
     *  all plugin objects
378
     * @return \CMS\Core\Package\PluginPackage|\Cake\Collection\Collection
379
     * @throws \Cake\Error\FatalErrorException When requested plugin was not found
380
     * @see \CMS\Core\Plugin::get()
381
     */
382
    function plugin($plugin = null)
383
    {
384
        return Plugin::get($plugin);
385
    }
386
}
387
388
if (!function_exists('theme')) {
389
    /**
390
     * Gets the given (or in use) theme as a package object.
391
     *
392
     * ### Example:
393
     *
394
     * ```php
395
     * // current theme
396
     * $bgColor = theme()->settings['background_color'];
397
     *
398
     * // specific theme
399
     * $bgColor = theme('BlueTheme')->settings['background_color'];
400
     * ```
401
     *
402
     * @param string|null $name Name of the theme to get, or null to get the theme
403
     *  being used in current request
404
     * @return \CMS\Core\Package\PluginPackage
405
     * @throws \Cake\Error\FatalErrorException When theme could not be found
406
     */
407
    function theme($name = null)
408
    {
409
        if ($name === null) {
410
            $option = Router::getRequest()->isAdmin() ? 'back_theme' : 'front_theme';
411
            $name = option($option);
412
        }
413
414
        $theme = Plugin::get()
415
            ->filter(function ($plugin) use ($name) {
416
                return $plugin->isTheme && $plugin->name == $name;
417
            })
418
            ->first();
419
420
        if ($theme) {
421
            return $theme;
422
        }
423
424
        throw new FatalErrorException(__d('cms', 'Theme "{0}" was not found', $name));
425
    }
426
}
427
428
if (!function_exists('listeners')) {
429
    /**
430
     * Returns a list of all registered event listeners within the provided event
431
     * manager, or within the global manager if not provided.
432
     *
433
     * @param \Cake\Event\EventManager\null $manager Event manager instance, or null
434
     *  to use global manager instance.
435
     * @return array
436
     */
437
    function listeners(EventManager $manager = null)
438
    {
439
        if ($manager === null) {
440
            $manager = EventManager::instance();
441
        }
442
        $class = new \ReflectionClass($manager);
443
        $property = $class->getProperty('_listeners');
444
        $property->setAccessible(true);
445
        $listeners = array_keys($property->getValue($manager));
446
447
        return $listeners;
448
    }
449
}
450
451
if (!function_exists('packageSplit')) {
452
    /**
453
     * Splits a composer package syntax into its vendor and package name.
454
     *
455
     * Commonly used like `list($vendor, $package) = packageSplit($name);`
456
     *
457
     * ### Example:
458
     *
459
     * ```php
460
     * list($vendor, $package) = packageSplit('some-vendor/this-package', true);
461
     * echo "{$vendor} : {$package}";
462
     * // prints: SomeVendor : ThisPackage
463
     * ```
464
     *
465
     * @param string $name Package name. e.g. author-name/package-name
466
     * @param bool $camelize Set to true to Camelize each part
467
     * @return array Array with 2 indexes. 0 => vendor name, 1 => package name.
468
     */
469
    function packageSplit($name, $camelize = false)
470
    {
471
        $pos = strrpos($name, '/');
472
        if ($pos === false) {
473
            $parts = ['', $name];
474
        } else {
475
            $parts = [substr($name, 0, $pos), substr($name, $pos + 1)];
476
        }
477
        if ($camelize) {
478
            $parts[0] = Inflector::camelize(str_replace('-', '_', $parts[0]));
479
            if (!empty($parts[1])) {
480
                $parts[1] = Inflector::camelize(str_replace('-', '_', $parts[1]));
481
            }
482
        }
483
484
        return $parts;
485
    }
486
}
487
488
if (!function_exists('normalizeLocale')) {
489
    /**
490
     * Normalizes the given locale code.
491
     *
492
     * @param string $locale The locale code to normalize. e.g. `en-US`
493
     * @return string Normalized code. e.g. `en_US`
494
     */
495
    function normalizeLocale($locale)
496
    {
497
        list($language, $region) = localeSplit($locale);
498
499
        return !empty($region) ? "{$language}_{$region}" : $language;
500
    }
501
}
502
503
if (!function_exists('aspects')) {
504
    /**
505
     * Gets a list of all active aspect classes.
506
     *
507
     * @return array
508
     */
509
    function aspects()
510
    {
511
        return quickapps('aspects');
512
    }
513
}
514
515
if (!function_exists('localeSplit')) {
516
    /**
517
     * Parses and splits the given locale code and returns its parts: language and
518
     * regional codes.
519
     *
520
     * ### Example:
521
     *
522
     * ```php
523
     * list($language, $region) = localeSplit('en_NZ');
524
     * ```
525
     *
526
     * IMPORTANT: Note that region code may be an empty string.
527
     *
528
     * @param string $localeId Locale code. e.g. "en_NZ" (or "en-NZ") for
529
     *  "English New Zealand"
530
     * @return array Array with 2 indexes. 0 => language code, 1 => country code.
531
     */
532
    function localeSplit($localeId)
533
    {
534
        $localeId = str_replace('-', '_', $localeId);
535
        $parts = explode('_', $localeId);
536
        $country = isset($parts[1]) ? strtoupper($parts[1]) : '';
537
        $language = strtolower($parts[0]);
538
539
        return [$language, $country];
540
    }
541
}
542
543
if (!function_exists('array_move')) {
544
    /**
545
     * Moves up or down the given element by index from a list array of elements.
546
     *
547
     * If item could not be moved, the original list will be returned. Valid values
548
     * for $direction are `up` or `down`.
549
     *
550
     * ### Example:
551
     *
552
     * ```php
553
     * array_move(['a', 'b', 'c'], 1, 'up');
554
     * // returns: ['a', 'c', 'b']
555
     * ```
556
     *
557
     * @param array $list Numeric indexed array list of elements
558
     * @param int $index The index position of the element you want to move
559
     * @param string $direction Direction, 'up' or 'down'
560
     * @return array Reordered original list.
561
     */
562
    function array_move(array $list, $index, $direction)
563
    {
564
        $maxIndex = count($list) - 1;
565
        if ($direction == 'down') {
566
            if (0 < $index && $index <= $maxIndex) {
567
                $item = $list[$index];
568
                $list[$index] = $list[$index - 1];
569
                $list[$index - 1] = $item;
570
            }
571
        } elseif ($direction == 'up') {
572
            if ($index >= 0 && $maxIndex > $index) {
573
                $item = $list[$index];
574
                $list[$index] = $list[$index + 1];
575
                $list[$index + 1] = $item;
576
577
                return $list;
578
            }
579
        }
580
581
        return $list;
582
    }
583
}
584
585
if (!function_exists('php_eval')) {
586
    /**
587
     * Evaluate a string of PHP code.
588
     *
589
     * This is a wrapper around PHP's eval(). It uses output buffering to capture both
590
     * returned and printed text. Unlike eval(), we require code to be surrounded by
591
     * <?php ?> tags; in other words, we evaluate the code as if it were a stand-alone
592
     * PHP file.
593
     *
594
     * Using this wrapper also ensures that the PHP code which is evaluated can not
595
     * overwrite any variables in the calling code, unlike a regular eval() call.
596
     *
597
     * ### Usage:
598
     *
599
     * ```php
600
     * echo php_eval('<?php return "Hello {$world}!"; ?>', ['world' => 'WORLD']);
601
     * // output: Hello WORLD
602
     * ```
603
     *
604
     * @param string $code The code to evaluate
605
     * @param array $args Array of arguments as `key` => `value` pairs, evaluated
606
     *  code can access this variables
607
     * @return string
608
     */
609
    function php_eval($code, $args = [])
610
    {
611
        ob_start();
612
        extract($args);
613
        print eval('?>' . $code);
614
        $output = ob_get_contents();
615
        ob_end_clean();
616
617
        return $output;
618
    }
619
}
620
621
if (!function_exists('get_this_class_methods')) {
622
    /**
623
     * Return only the methods for the given object. It will strip out inherited
624
     * methods.
625
     *
626
     * @param string $class Class name
627
     * @return array List of methods
628
     */
629
    function get_this_class_methods($class)
630
    {
631
        $primary = get_class_methods($class);
632
633
        if ($parent = get_parent_class($class)) {
634
            $secondary = get_class_methods($parent);
635
            $methods = array_diff($primary, $secondary);
636
        } else {
637
            $methods = $primary;
638
        }
639
640
        return $methods;
641
    }
642
}
643
644 View Code Duplication
if (!function_exists('str_replace_once')) {
645
    /**
646
     * Replace the first occurrence only.
647
     *
648
     * ### Example:
649
     *
650
     * ```php
651
     * echo str_replace_once('A', 'a', 'AAABBBCCC');
652
     * // out: aAABBBCCC
653
     * ```
654
     *
655
     * @param string|array $search The value being searched for
656
     * @param string $replace The replacement value that replaces found search value
657
     * @param string $subject The string being searched and replaced on
658
     * @return string A string with the replaced value
659
     */
660
    function str_replace_once($search, $replace, $subject)
661
    {
662
        if (!is_array($search)) {
663
            $search = [$search];
664
        }
665
666
        foreach ($search as $s) {
667
            if ($s !== '' && strpos($subject, $s) !== false) {
668
                return substr_replace($subject, $replace, strpos($subject, $s), strlen($s));
669
            }
670
        }
671
672
        return $subject;
673
    }
674
}
675
676 View Code Duplication
if (!function_exists('str_replace_last')) {
677
    /**
678
     * Replace the last occurrence only.
679
     *
680
     * ### Example:
681
     *
682
     * ```php
683
     * echo str_replace_once('A', 'a', 'AAABBBCCC');
684
     * // out: AAaBBBCCC
685
     * ```
686
     *
687
     * @param string|array $search The value being searched for
688
     * @param string $replace The replacement value that replaces found search value
689
     * @param string $subject The string being searched and replaced on
690
     * @return string A string with the replaced value
691
     */
692
    function str_replace_last($search, $replace, $subject)
693
    {
694
        if (!is_array($search)) {
695
            $search = [$search];
696
        }
697
698
        foreach ($search as $s) {
699
            if ($s !== '' && strrpos($subject, $s) !== false) {
700
                $subject = substr_replace($subject, $replace, strrpos($subject, $s), strlen($s));
701
            }
702
        }
703
704
        return $subject;
705
    }
706
}
707
708
if (!function_exists('str_starts_with')) {
709
    /**
710
     * Check if $haystack string starts with $needle string.
711
     *
712
     * ### Example:
713
     *
714
     * ```php
715
     * str_starts_with('lorem ipsum', 'lo'); // true
716
     * str_starts_with('lorem ipsum', 'ipsum'); // false
717
     * ```
718
     *
719
     * @param string $haystack The string to search in
720
     * @param string $needle The string to look for
721
     * @return bool
722
     */
723
    function str_starts_with($haystack, $needle)
724
    {
725
        return
726
            $needle === '' ||
727
            strpos($haystack, $needle) === 0;
728
    }
729
}
730
731
if (!function_exists('str_ends_with')) {
732
    /**
733
     * Check if $haystack string ends with $needle string.
734
     *
735
     * ### Example:
736
     *
737
     * ```php
738
     * str_ends_with('lorem ipsum', 'm'); // true
739
     * str_ends_with('dolorem sit amet', 'at'); // false
740
     * ```
741
     *
742
     * @param string $haystack The string to search in
743
     * @param string $needle The string to look for
744
     * @return bool
745
     */
746
    function str_ends_with($haystack, $needle)
747
    {
748
        return
749
            $needle === '' ||
750
            substr($haystack, - strlen($needle)) === $needle;
751
    }
752
}
753