Completed
Pull Request — 2.0 (#150)
by Christopher
02:42
created

functions.php ➔ option()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
c 0
b 0
f 0
nc 4
nop 2
dl 0
loc 18
rs 9.2
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')) {
53
            Cache::clear(false, 'default');
54
        }
55
56
        if (Cache::config('_cake_core_')) {
57
            Cache::clear(false, '_cake_core_');
58
        }
59
60
        if (Cache::config('_cake_model_')) {
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')) {
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']);
107
            $OptionsTable->schema(['value' => 'serialized']);
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, so you won't get a mix of "/" and "\" in your paths.
261
     *
262
     * ### Example:
263
     *
264
     * ```php
265
     * normalizePath('/some/path\to/some\\thing\about.zip');
266
     * // output: /some/path/to/some/thing/about.zip
267
     * ```
268
     *
269
     * You can indicate which "directory separator" symbol to use using the second
270
     * argument:
271
     *
272
     * ```php
273
     * normalizePath('/some/path\to//some\thing\about.zip', '\');
274
     * // output: \some\path\to\some\thing\about.zip
275
     * ```
276
     *
277
     * By defaults uses DIRECTORY_SEPARATOR as symbol.
278
     *
279
     * @param string $path The path to normalize
280
     * @param string $ds Directory separator character, defaults to DIRECTORY_SEPARATOR
281
     * @return string Normalized $path
282
     */
283
    function normalizePath($path, $ds = DIRECTORY_SEPARATOR)
284
    {
285
        $path = str_replace(['/', '\\', "{$ds}{$ds}"], $ds, $path);
286
        return str_replace("{$ds}{$ds}", $ds, $path);
287
    }
288
}
289
290
if (!function_exists('quickapps')) {
291
    /**
292
     * Shortcut for reading QuickApps's snapshot configuration.
293
     *
294
     * For example, `quickapps('variables');` maps to
295
     * `Configure::read('QuickApps.variables');`. If this function is used with
296
     * no arguments, `quickapps()`, the entire snapshot will be returned.
297
     *
298
     * @param string $key The key to read from snapshot, or null to read the whole
299
     *  snapshot's info
300
     * @return mixed
301
     */
302
    function quickapps($key = null)
303
    {
304
        if ($key !== null) {
305
            return Configure::read("QuickApps.{$key}");
306
        }
307
        return Configure::read('QuickApps');
308
    }
309
}
310
311
if (!function_exists('option')) {
312
    /**
313
     * Shortcut for getting an option value from "options" DB table.
314
     *
315
     * The second arguments, $default,  is used as default value to return if no
316
     * value is found. If not value is found and not default values was given this
317
     * function will return `false`.
318
     *
319
     * ### Example:
320
     *
321
     * ```php
322
     * option('site_slogan');
323
     * ```
324
     *
325
     * @param string $name Name of the option to retrieve. e.g. `front_theme`,
326
     *  `default_language`, `site_slogan`, etc
327
     * @param mixed $default The default value to return if no value is found
328
     * @return mixed Current value for the specified option. If the specified option
329
     *  does not exist, returns boolean FALSE
330
     */
331
    function option($name, $default = false)
332
    {
333
        if (Configure::check("QuickApps.options.{$name}")) {
334
            return Configure::read("QuickApps.options.{$name}");
335
        }
336
337
        if (ConnectionManager::config('default')) {
338
            $option = TableRegistry::get('Options')
339
                ->find()
340
                ->where(['Options.name' => $name])
341
                ->first();
342
            if ($option) {
343
                return $option->value;
344
            }
345
        }
346
347
        return $default;
348
    }
349
}
350
351
if (!function_exists('plugin')) {
352
    /**
353
     * Shortcut for "Plugin::get()".
354
     *
355
     * ### Example:
356
     *
357
     * ```php
358
     * $specialSetting = plugin('MyPlugin')->settings['special_setting'];
359
     * ```
360
     *
361
     * @param string $plugin Plugin name to get, or null to get a collection of
362
     *  all plugin objects
363
     * @return \CMS\Core\Package\PluginPackage|\Cake\Collection\Collection
364
     * @throws \Cake\Error\FatalErrorException When requested plugin was not found
365
     * @see \CMS\Core\Plugin::get()
366
     */
367
    function plugin($plugin = null)
368
    {
369
        return Plugin::get($plugin);
370
    }
371
}
372
373
if (!function_exists('theme')) {
374
    /**
375
     * Gets the given (or in use) theme as a package object.
376
     *
377
     * ### Example:
378
     *
379
     * ```php
380
     * // current theme
381
     * $bgColor = theme()->settings['background_color'];
382
     *
383
     * // specific theme
384
     * $bgColor = theme('BlueTheme')->settings['background_color'];
385
     * ```
386
     *
387
     * @param string|null $name Name of the theme to get, or null to get the theme
388
     *  being used in current request
389
     * @return \CMS\Core\Package\PluginPackage
390
     * @throws \Cake\Error\FatalErrorException When theme could not be found
391
     */
392
    function theme($name = null)
393
    {
394
        if ($name === null) {
395
            $option = Router::getRequest()->isAdmin() ? 'back_theme' : 'front_theme';
396
            $name = option($option);
397
        }
398
399
        $theme = Plugin::get()
400
            ->filter(function ($plugin) use ($name) {
401
                return $plugin->isTheme && $plugin->name == $name;
402
            })
403
            ->first();
404
405
        if ($theme) {
406
            return $theme;
407
        }
408
409
        throw new FatalErrorException(__d('cms', 'Theme "{0}" was not found', $name));
410
    }
411
}
412
413
if (!function_exists('listeners')) {
414
    /**
415
     * Returns a list of all registered event listeners within the provided event
416
     * manager, or within the global manager if not provided.
417
     *
418
     * @param \Cake\Event\EventManager\null $manager Event manager instance, or null
419
     *  to use global manager instance.
420
     * @return array
421
     */
422
    function listeners(EventManager $manager = null)
423
    {
424
        if ($manager === null) {
425
            $manager = EventManager::instance();
426
        }
427
        $class = new \ReflectionClass($manager);
428
        $property = $class->getProperty('_listeners');
429
        $property->setAccessible(true);
430
        $listeners = array_keys($property->getValue($manager));
431
        return $listeners;
432
    }
433
}
434
435
if (!function_exists('packageSplit')) {
436
    /**
437
     * Splits a composer package syntax into its vendor and package name.
438
     *
439
     * Commonly used like `list($vendor, $package) = packageSplit($name);`
440
     *
441
     * ### Example:
442
     *
443
     * ```php
444
     * list($vendor, $package) = packageSplit('some-vendor/this-package', true);
445
     * echo "{$vendor} : {$package}";
446
     * // prints: SomeVendor : ThisPackage
447
     * ```
448
     *
449
     * @param string $name Package name. e.g. author-name/package-name
450
     * @param bool $camelize Set to true to Camelize each part
451
     * @return array Array with 2 indexes. 0 => vendor name, 1 => package name.
452
     */
453
    function packageSplit($name, $camelize = false)
454
    {
455
        $pos = strrpos($name, '/');
456
        if ($pos === false) {
457
            $parts = ['', $name];
458
        } else {
459
            $parts = [substr($name, 0, $pos), substr($name, $pos + 1)];
460
        }
461
        if ($camelize) {
462
            $parts[0] = Inflector::camelize(str_replace('-', '_', $parts[0]));
463
            if (!empty($parts[1])) {
464
                $parts[1] = Inflector::camelize(str_replace('-', '_', $parts[1]));
465
            }
466
        }
467
        return $parts;
468
    }
469
}
470
471
if (!function_exists('normalizeLocale')) {
472
    /**
473
     * Normalizes the given locale code.
474
     *
475
     * @param string $locale The locale code to normalize. e.g. `en-US`
476
     * @return string Normalized code. e.g. `en_US`
477
     */
478
    function normalizeLocale($locale)
479
    {
480
        list($language, $region) = localeSplit($locale);
481
        return !empty($region) ? "{$language}_{$region}" : $language;
482
    }
483
}
484
485
if (!function_exists('aspects')) {
486
    /**
487
     * Gets a list of all active aspect classes.
488
     *
489
     * @return array
490
     */
491
    function aspects()
492
    {
493
        return quickapps('aspects');
494
    }
495
}
496
497
if (!function_exists('localeSplit')) {
498
    /**
499
     * Parses and splits the given locale code and returns its parts: language and
500
     * regional codes.
501
     *
502
     * ### Example:
503
     *
504
     * ```php
505
     * list($language, $region) = localeSplit('en_NZ');
506
     * ```
507
     *
508
     * IMPORTANT: Note that region code may be an empty string.
509
     *
510
     * @param string $localeId Locale code. e.g. "en_NZ" (or "en-NZ") for
511
     *  "English New Zealand"
512
     * @return array Array with 2 indexes. 0 => language code, 1 => country code.
513
     */
514
    function localeSplit($localeId)
515
    {
516
        $localeId = str_replace('-', '_', $localeId);
517
        $parts = explode('_', $localeId);
518
        $country = isset($parts[1]) ? strtoupper($parts[1]) : '';
519
        $language = strtolower($parts[0]);
520
        return [$language, $country];
521
    }
522
}
523
524
if (!function_exists('array_move')) {
525
    /**
526
     * Moves up or down the given element by index from a list array of elements.
527
     *
528
     * If item could not be moved, the original list will be returned. Valid values
529
     * for $direction are `up` or `down`.
530
     *
531
     * ### Example:
532
     *
533
     * ```php
534
     * array_move(['a', 'b', 'c'], 1, 'up');
535
     * // returns: ['a', 'c', 'b']
536
     * ```
537
     *
538
     * @param array $list Numeric indexed array list of elements
539
     * @param int $index The index position of the element you want to move
540
     * @param string $direction Direction, 'up' or 'down'
541
     * @return array Reordered original list.
542
     */
543
    function array_move(array $list, $index, $direction)
544
    {
545
        $maxIndex = count($list) - 1;
546
        if ($direction == 'down') {
547
            if (0 < $index && $index <= $maxIndex) {
548
                $item = $list[$index];
549
                $list[$index] = $list[$index - 1];
550
                $list[$index - 1] = $item;
551
            }
552
        } elseif ($direction == 'up') {
553
            if ($index >= 0 && $maxIndex > $index) {
554
                $item = $list[$index];
555
                $list[$index] = $list[$index + 1];
556
                $list[$index + 1] = $item;
557
                return $list;
558
            }
559
        }
560
561
        return $list;
562
    }
563
}
564
565
if (!function_exists('php_eval')) {
566
    /**
567
     * Evaluate a string of PHP code.
568
     *
569
     * This is a wrapper around PHP's eval(). It uses output buffering to capture both
570
     * returned and printed text. Unlike eval(), we require code to be surrounded by
571
     * <?php ?> tags; in other words, we evaluate the code as if it were a stand-alone
572
     * PHP file.
573
     *
574
     * Using this wrapper also ensures that the PHP code which is evaluated can not
575
     * overwrite any variables in the calling code, unlike a regular eval() call.
576
     *
577
     * ### Usage:
578
     *
579
     * ```php
580
     * echo php_eval('<?php return "Hello {$world}!"; ?>', ['world' => 'WORLD']);
581
     * // output: Hello WORLD
582
     * ```
583
     *
584
     * @param string $code The code to evaluate
585
     * @param array $args Array of arguments as `key` => `value` pairs, evaluated
586
     *  code can access this variables
587
     * @return string
588
     */
589
    function php_eval($code, $args = [])
590
    {
591
        ob_start();
592
        extract($args);
593
        print eval('?>' . $code);
594
        $output = ob_get_contents();
595
        ob_end_clean();
596
        return $output;
597
    }
598
}
599
600
if (!function_exists('get_this_class_methods')) {
601
    /**
602
     * Return only the methods for the given object. It will strip out inherited
603
     * methods.
604
     *
605
     * @param string $class Class name
606
     * @return array List of methods
607
     */
608
    function get_this_class_methods($class)
609
    {
610
        $primary = get_class_methods($class);
611
612
        if ($parent = get_parent_class($class)) {
613
            $secondary = get_class_methods($parent);
614
            $methods = array_diff($primary, $secondary);
615
        } else {
616
            $methods = $primary;
617
        }
618
619
        return $methods;
620
    }
621
}
622
623 View Code Duplication
if (!function_exists('str_replace_once')) {
624
    /**
625
     * Replace the first occurrence only.
626
     *
627
     * ### Example:
628
     *
629
     * ```php
630
     * echo str_replace_once('A', 'a', 'AAABBBCCC');
631
     * // out: aAABBBCCC
632
     * ```
633
     *
634
     * @param string|array $search The value being searched for
635
     * @param string $replace The replacement value that replaces found search value
636
     * @param string $subject The string being searched and replaced on
637
     * @return string A string with the replaced value
638
     */
639
    function str_replace_once($search, $replace, $subject)
640
    {
641
        if (!is_array($search)) {
642
            $search = [$search];
643
        }
644
645
        foreach ($search as $s) {
646
            if ($s !== '' && strpos($subject, $s) !== false) {
647
                return substr_replace($subject, $replace, strpos($subject, $s), strlen($s));
648
            }
649
        }
650
651
        return $subject;
652
    }
653
}
654
655 View Code Duplication
if (!function_exists('str_replace_last')) {
656
    /**
657
     * Replace the last occurrence only.
658
     *
659
     * ### Example:
660
     *
661
     * ```php
662
     * echo str_replace_once('A', 'a', 'AAABBBCCC');
663
     * // out: AAaBBBCCC
664
     * ```
665
     *
666
     * @param string|array $search The value being searched for
667
     * @param string $replace The replacement value that replaces found search value
668
     * @param string $subject The string being searched and replaced on
669
     * @return string A string with the replaced value
670
     */
671
    function str_replace_last($search, $replace, $subject)
672
    {
673
        if (!is_array($search)) {
674
            $search = [$search];
675
        }
676
677
        foreach ($search as $s) {
678
            if ($s !== '' && strrpos($subject, $s) !== false) {
679
                $subject = substr_replace($subject, $replace, strrpos($subject, $s), strlen($s));
680
            }
681
        }
682
683
        return $subject;
684
    }
685
}
686
687
if (!function_exists('str_starts_with')) {
688
    /**
689
     * Check if $haystack string starts with $needle string.
690
     *
691
     * ### Example:
692
     *
693
     * ```php
694
     * str_starts_with('lorem ipsum', 'lo'); // true
695
     * str_starts_with('lorem ipsum', 'ipsum'); // false
696
     * ```
697
     *
698
     * @param string $haystack The string to search in
699
     * @param string $needle The string to look for
700
     * @return bool
701
     */
702
    function str_starts_with($haystack, $needle)
703
    {
704
        return
705
            $needle === '' ||
706
            strpos($haystack, $needle) === 0;
707
    }
708
}
709
710
if (!function_exists('str_ends_with')) {
711
    /**
712
     * Check if $haystack string ends with $needle string.
713
     *
714
     * ### Example:
715
     *
716
     * ```php
717
     * str_ends_with('lorem ipsum', 'm'); // true
718
     * str_ends_with('dolorem sit amet', 'at'); // false
719
     * ```
720
     *
721
     * @param string $haystack The string to search in
722
     * @param string $needle The string to look for
723
     * @return bool
724
     */
725
    function str_ends_with($haystack, $needle)
726
    {
727
        return
728
            $needle === '' ||
729
            substr($haystack, - strlen($needle)) === $needle;
730
    }
731
}
732