PageRenderTime 64ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 0ms

/plugins/CMS/config/functions.php

http://github.com/QuickAppsCMS/QuickApps-CMS
PHP | 752 lines | 587 code | 42 blank | 123 comment | 44 complexity | 66cce543626f763d3655b8b9cce9e3c1 MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception, GPL-3.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 <chris@quickapps.es>
  9. * @link http://www.quickappscms.org
  10. * @license http://opensource.org/licenses/gpl-3.0.html GPL-3.0 License
  11. */
  12. use Cake\Cache\Cache;
  13. use Cake\Core\Configure;
  14. use Cake\Datasource\ConnectionManager;
  15. use Cake\Error\Debugger;
  16. use Cake\Error\FatalErrorException;
  17. use Cake\Event\EventManager;
  18. use Cake\Filesystem\File;
  19. use Cake\Filesystem\Folder;
  20. use Cake\I18n\I18n;
  21. use Cake\ORM\Entity;
  22. use Cake\ORM\TableRegistry;
  23. use Cake\Routing\Router;
  24. use Cake\Utility\Inflector;
  25. use CMS\Core\Plugin;
  26. if (!function_exists('snapshot')) {
  27. /**
  28. * Stores some bootstrap-handy information into a persistent file.
  29. *
  30. * Information is stored in `TMP/snapshot.php` file, it contains
  31. * useful information such as enabled languages, content types slugs, installed
  32. * plugins, etc.
  33. *
  34. * You can read this information using `Configure::read()` as follow:
  35. *
  36. * ```php
  37. * Configure::read('QuickApps.<option>');
  38. * ```
  39. *
  40. * Or using the `quickapps()` global function:
  41. *
  42. * ```php
  43. * quickapps('<option>');
  44. * ```
  45. *
  46. * @return void
  47. */
  48. function snapshot()
  49. {
  50. if (Cache::config('default')) {
  51. Cache::clear(false, 'default');
  52. }
  53. if (Cache::config('_cake_core_')) {
  54. Cache::clear(false, '_cake_core_');
  55. }
  56. if (Cache::config('_cake_model_')) {
  57. Cache::clear(false, '_cake_model_');
  58. }
  59. $versionPath = QUICKAPPS_CORE . 'VERSION.txt';
  60. $snapshot = [
  61. 'version' => null,
  62. 'content_types' => [],
  63. 'plugins' => [],
  64. 'options' => [],
  65. 'languages' => [],
  66. 'aspects' => [],
  67. ];
  68. if (is_readable($versionPath)) {
  69. $versionFile = file($versionPath);
  70. $snapshot['version'] = trim(array_pop($versionFile));
  71. } else {
  72. die(sprintf('Missing file: %s', $versionPath));
  73. }
  74. if (ConnectionManager::config('default')) {
  75. if (!TableRegistry::exists('SnapshotPlugins')) {
  76. $PluginTable = TableRegistry::get('SnapshotPlugins', ['table' => 'plugins']);
  77. } else {
  78. $PluginTable = TableRegistry::get('SnapshotPlugins');
  79. }
  80. if (!TableRegistry::exists('SnapshotContentTypes')) {
  81. $ContentTypesTable = TableRegistry::get('SnapshotContentTypes', ['table' => 'content_types']);
  82. } else {
  83. $ContentTypesTable = TableRegistry::get('SnapshotContentTypes');
  84. }
  85. if (!TableRegistry::exists('SnapshotLanguages')) {
  86. $LanguagesTable = TableRegistry::get('SnapshotLanguages', ['table' => 'languages']);
  87. } else {
  88. $LanguagesTable = TableRegistry::get('SnapshotLanguages');
  89. }
  90. if (!TableRegistry::exists('SnapshotOptions')) {
  91. $OptionsTable = TableRegistry::get('SnapshotOptions', ['table' => 'options']);
  92. } else {
  93. $OptionsTable = TableRegistry::get('SnapshotOptions');
  94. }
  95. $PluginTable->schema(['value' => 'serialized']);
  96. $OptionsTable->schema(['value' => 'serialized']);
  97. $plugins = $PluginTable->find()
  98. ->select(['name', 'package', 'status'])
  99. ->order([
  100. 'ordering' => 'ASC',
  101. 'name' => 'ASC',
  102. ])
  103. ->all();
  104. $contentTypes = $ContentTypesTable->find()
  105. ->select(['slug'])
  106. ->all();
  107. $languages = $LanguagesTable->find()
  108. ->where(['status' => 1])
  109. ->order(['ordering' => 'ASC'])
  110. ->all();
  111. $options = $OptionsTable->find()
  112. ->select(['name', 'value'])
  113. ->where(['autoload' => 1])
  114. ->all();
  115. foreach ($contentTypes as $contentType) {
  116. $snapshot['content_types'][] = $contentType->slug;
  117. }
  118. foreach ($options as $option) {
  119. $snapshot['options'][$option->name] = $option->value;
  120. }
  121. foreach ($languages as $language) {
  122. list($languageCode, $countryCode) = localeSplit($language->code);
  123. $snapshot['languages'][$language->code] = [
  124. 'name' => $language->name,
  125. 'locale' => $language->code,
  126. 'code' => $languageCode,
  127. 'country' => $countryCode,
  128. 'direction' => $language->direction,
  129. 'icon' => $language->icon,
  130. ];
  131. }
  132. } else {
  133. $plugins = [];
  134. foreach (Plugin::scan() as $plugin => $path) {
  135. $plugins[] = new Entity([
  136. 'name' => $plugin,
  137. 'status' => true,
  138. 'package' => 'quickapps-plugins',
  139. ]);
  140. }
  141. }
  142. $folder = new Folder(QUICKAPPS_CORE . 'src/Aspect/');
  143. foreach ($folder->read(false, false, true)[1] as $classFile) {
  144. $className = basename(preg_replace('/\.php$/', '', $classFile));
  145. if (!in_array($className, ['AppAspect', 'Aspect'])) {
  146. $snapshot['aspects'][] = "CMS\\Aspect\\{$className}";
  147. }
  148. }
  149. foreach ($plugins as $plugin) {
  150. $pluginPath = false;
  151. if (isset(Plugin::scan()[$plugin->name])) {
  152. $pluginPath = Plugin::scan()[$plugin->name];
  153. }
  154. if ($pluginPath === false) {
  155. Debugger::log(sprintf('Plugin "%s" was found in DB but QuickAppsCMS was unable to locate its root directory.', $plugin->name));
  156. continue;
  157. }
  158. if (!Plugin::validateJson("{$pluginPath}/composer.json")) {
  159. Debugger::log(sprintf('Plugin "%s" has a corrupt "composer.json" file (%s).', $plugin->name, "{$pluginPath}/composer.json"));
  160. continue;
  161. }
  162. $aspectsPath = "{$pluginPath}/src/Aspect/";
  163. $eventsPath = "{$pluginPath}/src/Event/";
  164. $fieldsPath = "{$pluginPath}/src/Field/";
  165. $helpFiles = glob($pluginPath . '/src/Template/Element/Help/help*.ctp');
  166. $isTheme = str_ends_with($plugin->name, 'Theme');
  167. $status = (bool)$plugin->status;
  168. $humanName = '';
  169. $aspects = [];
  170. $eventListeners = [];
  171. $fields = [];
  172. $subspaces = [
  173. $aspectsPath => 'Aspect',
  174. $eventsPath => 'Event',
  175. $fieldsPath => 'Field',
  176. ];
  177. $varnames = [
  178. $aspectsPath => 'aspects',
  179. $eventsPath => 'eventListeners',
  180. $fieldsPath => 'fields',
  181. ];
  182. foreach ([$aspectsPath, $eventsPath, $fieldsPath] as $path) {
  183. if (is_dir($path)) {
  184. $Folder = new Folder($path);
  185. foreach ($Folder->read(false, false, true)[1] as $classFile) {
  186. $className = basename(preg_replace('/\.php$/', '', $classFile));
  187. $subspace = $subspaces[$path];
  188. $varname = $varnames[$path];
  189. $namespace = "{$plugin->name}\\{$subspace}\\";
  190. ${$varname}[] = $namespace . $className;
  191. }
  192. }
  193. }
  194. if (is_readable("{$pluginPath}composer.json")) {
  195. $json = (array)json_decode(file_get_contents("{$pluginPath}composer.json"), true);
  196. if (!empty($json['extra']['human-name'])) {
  197. $humanName = $json['extra']['human-name'];
  198. }
  199. }
  200. if (empty($humanName)) {
  201. $humanName = (string)Inflector::humanize((string)Inflector::underscore($plugin->name));
  202. if ($isTheme) {
  203. $humanName = trim(str_replace_last('Theme', '', $humanName));
  204. }
  205. }
  206. $snapshot['plugins'][$plugin->name] = [
  207. 'name' => $plugin->name,
  208. 'humanName' => $humanName,
  209. 'package' => $plugin->package,
  210. 'isTheme' => $isTheme,
  211. 'hasHelp' => !empty($helpFiles),
  212. 'hasSettings' => is_readable($pluginPath . '/src/Template/Element/settings.ctp'),
  213. 'aspects' => $aspects,
  214. 'eventListeners' => $eventListeners,
  215. 'fields' => $fields,
  216. 'status' => $status,
  217. 'path' => $pluginPath,
  218. ];
  219. if ($status) {
  220. $snapshot['aspects'] = array_merge($snapshot['aspects'], $aspects);
  221. }
  222. }
  223. Configure::write('QuickApps', $snapshot);
  224. if (!Configure::dump('snapshot', 'QuickApps', ['QuickApps'])) {
  225. die('QuickAppsCMS was unable to create a snapshot file, check that PHP have permission to write to the "/tmp" directory.');
  226. }
  227. }
  228. }
  229. if (!function_exists('normalizePath')) {
  230. /**
  231. * Normalizes the given file system path, makes sure that all DIRECTORY_SEPARATOR
  232. * are the same according to current OS, so you won't get a mix of "/" and "\" in
  233. * your paths.
  234. *
  235. * ### Example:
  236. *
  237. * ```php
  238. * normalizePath('/path\to/filename\with\backslash.zip');
  239. * // output LINUX: /path/to/filename\with\backslashes.zip
  240. * // output WINDOWS: /path/to/filename/with/backslashes.zip
  241. * ```
  242. *
  243. * You can indicate which "directory separator" symbol to use using the second
  244. * argument:
  245. *
  246. * ```php
  247. * normalizePath('/path\to/filename\with\backslash.zip', '\');
  248. * // output LINUX & WIDNOWS: \path\to\filename\with\backslash.zip
  249. * ```
  250. *
  251. * By defaults uses DIRECTORY_SEPARATOR as symbol.
  252. *
  253. * @param string $path The path to normalize
  254. * @param string $ds Directory separator character, defaults to DIRECTORY_SEPARATOR
  255. * @return string Normalized $path
  256. */
  257. function normalizePath($path, $ds = DIRECTORY_SEPARATOR)
  258. {
  259. $tail = '';
  260. $base = $path;
  261. if (DIRECTORY_SEPARATOR === '/') {
  262. $lastDS = strrpos($path, $ds);
  263. $tail = $lastDS !== false && $lastDS !== strlen($path) - 1 ? substr($path, $lastDS + 1) : '';
  264. $base = $tail ? substr($path, 0, $lastDS + 1) : $path;
  265. }
  266. $path = str_replace(['/', '\\', "{$ds}{$ds}"], $ds, $base);
  267. $path = str_replace("{$ds}{$ds}", $ds, $path);
  268. $path .= $tail;
  269. return $path;
  270. }
  271. }
  272. if (!function_exists('quickapps')) {
  273. /**
  274. * Shortcut for reading QuickApps's snapshot configuration.
  275. *
  276. * For example, `quickapps('variables');` maps to
  277. * `Configure::read('QuickApps.variables');`. If this function is used with
  278. * no arguments, `quickapps()`, the entire snapshot will be returned.
  279. *
  280. * @param string $key The key to read from snapshot, or null to read the whole
  281. * snapshot's info
  282. * @return mixed
  283. */
  284. function quickapps($key = null)
  285. {
  286. if ($key !== null) {
  287. return Configure::read("QuickApps.{$key}");
  288. }
  289. return Configure::read('QuickApps');
  290. }
  291. }
  292. if (!function_exists('option')) {
  293. /**
  294. * Shortcut for getting an option value from "options" DB table.
  295. *
  296. * The second arguments, $default, is used as default value to return if no
  297. * value is found. If not value is found and not default values was given this
  298. * function will return `false`.
  299. *
  300. * ### Example:
  301. *
  302. * ```php
  303. * option('site_slogan');
  304. * ```
  305. *
  306. * @param string $name Name of the option to retrieve. e.g. `front_theme`,
  307. * `default_language`, `site_slogan`, etc
  308. * @param mixed $default The default value to return if no value is found
  309. * @return mixed Current value for the specified option. If the specified option
  310. * does not exist, returns boolean FALSE
  311. */
  312. function option($name, $default = false)
  313. {
  314. if (Configure::check("QuickApps.options.{$name}")) {
  315. return Configure::read("QuickApps.options.{$name}");
  316. }
  317. if (ConnectionManager::config('default')) {
  318. $option = TableRegistry::get('Options')
  319. ->find()
  320. ->where(['Options.name' => $name])
  321. ->first();
  322. if ($option) {
  323. return $option->value;
  324. }
  325. }
  326. return $default;
  327. }
  328. }
  329. if (!function_exists('plugin')) {
  330. /**
  331. * Shortcut for "Plugin::get()".
  332. *
  333. * ### Example:
  334. *
  335. * ```php
  336. * $specialSetting = plugin('MyPlugin')->settings['special_setting'];
  337. * ```
  338. *
  339. * @param string $plugin Plugin name to get, or null to get a collection of
  340. * all plugin objects
  341. * @return \CMS\Core\Package\PluginPackage|\Cake\Collection\Collection
  342. * @throws \Cake\Error\FatalErrorException When requested plugin was not found
  343. * @see \CMS\Core\Plugin::get()
  344. */
  345. function plugin($plugin = null)
  346. {
  347. return Plugin::get($plugin);
  348. }
  349. }
  350. if (!function_exists('theme')) {
  351. /**
  352. * Gets the given (or in use) theme as a package object.
  353. *
  354. * ### Example:
  355. *
  356. * ```php
  357. * // current theme
  358. * $bgColor = theme()->settings['background_color'];
  359. *
  360. * // specific theme
  361. * $bgColor = theme('BlueTheme')->settings['background_color'];
  362. * ```
  363. *
  364. * @param string|null $name Name of the theme to get, or null to get the theme
  365. * being used in current request
  366. * @return \CMS\Core\Package\PluginPackage
  367. * @throws \Cake\Error\FatalErrorException When theme could not be found
  368. */
  369. function theme($name = null)
  370. {
  371. if ($name === null) {
  372. $option = Router::getRequest()->isAdmin() ? 'back_theme' : 'front_theme';
  373. $name = option($option);
  374. }
  375. $theme = Plugin::get()
  376. ->filter(function ($plugin) use ($name) {
  377. return $plugin->isTheme && $plugin->name == $name;
  378. })
  379. ->first();
  380. if ($theme) {
  381. return $theme;
  382. }
  383. throw new FatalErrorException(__d('cms', 'Theme "{0}" was not found', $name));
  384. }
  385. }
  386. if (!function_exists('listeners')) {
  387. /**
  388. * Returns a list of all registered event listeners within the provided event
  389. * manager, or within the global manager if not provided.
  390. *
  391. * @param \Cake\Event\EventManager\null $manager Event manager instance, or null
  392. * to use global manager instance.
  393. * @return array
  394. */
  395. function listeners(EventManager $manager = null)
  396. {
  397. if ($manager === null) {
  398. $manager = EventManager::instance();
  399. }
  400. $class = new \ReflectionClass($manager);
  401. $property = $class->getProperty('_listeners');
  402. $property->setAccessible(true);
  403. $listeners = array_keys($property->getValue($manager));
  404. return $listeners;
  405. }
  406. }
  407. if (!function_exists('packageSplit')) {
  408. /**
  409. * Splits a composer package syntax into its vendor and package name.
  410. *
  411. * Commonly used like `list($vendor, $package) = packageSplit($name);`
  412. *
  413. * ### Example:
  414. *
  415. * ```php
  416. * list($vendor, $package) = packageSplit('some-vendor/this-package', true);
  417. * echo "{$vendor} : {$package}";
  418. * // prints: SomeVendor : ThisPackage
  419. * ```
  420. *
  421. * @param string $name Package name. e.g. author-name/package-name
  422. * @param bool $camelize Set to true to Camelize each part
  423. * @return array Array with 2 indexes. 0 => vendor name, 1 => package name.
  424. */
  425. function packageSplit($name, $camelize = false)
  426. {
  427. $pos = strrpos($name, '/');
  428. if ($pos === false) {
  429. $parts = ['', $name];
  430. } else {
  431. $parts = [substr($name, 0, $pos), substr($name, $pos + 1)];
  432. }
  433. if ($camelize) {
  434. $parts[0] = Inflector::camelize(str_replace('-', '_', $parts[0]));
  435. if (!empty($parts[1])) {
  436. $parts[1] = Inflector::camelize(str_replace('-', '_', $parts[1]));
  437. }
  438. }
  439. return $parts;
  440. }
  441. }
  442. if (!function_exists('normalizeLocale')) {
  443. /**
  444. * Normalizes the given locale code.
  445. *
  446. * @param string $locale The locale code to normalize. e.g. `en-US`
  447. * @return string Normalized code. e.g. `en_US`
  448. */
  449. function normalizeLocale($locale)
  450. {
  451. list($language, $region) = localeSplit($locale);
  452. return !empty($region) ? "{$language}_{$region}" : $language;
  453. }
  454. }
  455. if (!function_exists('aspects')) {
  456. /**
  457. * Gets a list of all active aspect classes.
  458. *
  459. * @return array
  460. */
  461. function aspects()
  462. {
  463. return quickapps('aspects');
  464. }
  465. }
  466. if (!function_exists('localeSplit')) {
  467. /**
  468. * Parses and splits the given locale code and returns its parts: language and
  469. * regional codes.
  470. *
  471. * ### Example:
  472. *
  473. * ```php
  474. * list($language, $region) = localeSplit('en_NZ');
  475. * ```
  476. *
  477. * IMPORTANT: Note that region code may be an empty string.
  478. *
  479. * @param string $localeId Locale code. e.g. "en_NZ" (or "en-NZ") for
  480. * "English New Zealand"
  481. * @return array Array with 2 indexes. 0 => language code, 1 => country code.
  482. */
  483. function localeSplit($localeId)
  484. {
  485. $localeId = str_replace('-', '_', $localeId);
  486. $parts = explode('_', $localeId);
  487. $country = isset($parts[1]) ? strtoupper($parts[1]) : '';
  488. $language = strtolower($parts[0]);
  489. return [$language, $country];
  490. }
  491. }
  492. if (!function_exists('array_move')) {
  493. /**
  494. * Moves up or down the given element by index from a list array of elements.
  495. *
  496. * If item could not be moved, the original list will be returned. Valid values
  497. * for $direction are `up` or `down`.
  498. *
  499. * ### Example:
  500. *
  501. * ```php
  502. * array_move(['a', 'b', 'c'], 1, 'up');
  503. * // returns: ['a', 'c', 'b']
  504. * ```
  505. *
  506. * @param array $list Numeric indexed array list of elements
  507. * @param int $index The index position of the element you want to move
  508. * @param string $direction Direction, 'up' or 'down'
  509. * @return array Reordered original list.
  510. */
  511. function array_move(array $list, $index, $direction)
  512. {
  513. $maxIndex = count($list) - 1;
  514. if ($direction == 'down') {
  515. if (0 < $index && $index <= $maxIndex) {
  516. $item = $list[$index];
  517. $list[$index] = $list[$index - 1];
  518. $list[$index - 1] = $item;
  519. }
  520. } elseif ($direction == 'up') {
  521. if ($index >= 0 && $maxIndex > $index) {
  522. $item = $list[$index];
  523. $list[$index] = $list[$index + 1];
  524. $list[$index + 1] = $item;
  525. return $list;
  526. }
  527. }
  528. return $list;
  529. }
  530. }
  531. if (!function_exists('php_eval')) {
  532. /**
  533. * Evaluate a string of PHP code.
  534. *
  535. * This is a wrapper around PHP's eval(). It uses output buffering to capture both
  536. * returned and printed text. Unlike eval(), we require code to be surrounded by
  537. * <?php ?> tags; in other words, we evaluate the code as if it were a stand-alone
  538. * PHP file.
  539. *
  540. * Using this wrapper also ensures that the PHP code which is evaluated can not
  541. * overwrite any variables in the calling code, unlike a regular eval() call.
  542. *
  543. * ### Usage:
  544. *
  545. * ```php
  546. * echo php_eval('<?php return "Hello {$world}!"; ?>', ['world' => 'WORLD']);
  547. * // output: Hello WORLD
  548. * ```
  549. *
  550. * @param string $code The code to evaluate
  551. * @param array $args Array of arguments as `key` => `value` pairs, evaluated
  552. * code can access this variables
  553. * @return string
  554. */
  555. function php_eval($code, $args = [])
  556. {
  557. ob_start();
  558. extract($args);
  559. print eval('?>' . $code);
  560. $output = ob_get_contents();
  561. ob_end_clean();
  562. return $output;
  563. }
  564. }
  565. if (!function_exists('get_this_class_methods')) {
  566. /**
  567. * Return only the methods for the given object. It will strip out inherited
  568. * methods.
  569. *
  570. * @param string $class Class name
  571. * @return array List of methods
  572. */
  573. function get_this_class_methods($class)
  574. {
  575. $primary = get_class_methods($class);
  576. if ($parent = get_parent_class($class)) {
  577. $secondary = get_class_methods($parent);
  578. $methods = array_diff($primary, $secondary);
  579. } else {
  580. $methods = $primary;
  581. }
  582. return $methods;
  583. }
  584. }
  585. if (!function_exists('str_replace_once')) {
  586. /**
  587. * Replace the first occurrence only.
  588. *
  589. * ### Example:
  590. *
  591. * ```php
  592. * echo str_replace_once('A', 'a', 'AAABBBCCC');
  593. * // out: aAABBBCCC
  594. * ```
  595. *
  596. * @param string|array $search The value being searched for
  597. * @param string $replace The replacement value that replaces found search value
  598. * @param string $subject The string being searched and replaced on
  599. * @return string A string with the replaced value
  600. */
  601. function str_replace_once($search, $replace, $subject)
  602. {
  603. if (!is_array($search)) {
  604. $search = [$search];
  605. }
  606. foreach ($search as $s) {
  607. if ($s !== '' && strpos($subject, $s) !== false) {
  608. return substr_replace($subject, $replace, strpos($subject, $s), strlen($s));
  609. }
  610. }
  611. return $subject;
  612. }
  613. }
  614. if (!function_exists('str_replace_last')) {
  615. /**
  616. * Replace the last occurrence only.
  617. *
  618. * ### Example:
  619. *
  620. * ```php
  621. * echo str_replace_once('A', 'a', 'AAABBBCCC');
  622. * // out: AAaBBBCCC
  623. * ```
  624. *
  625. * @param string|array $search The value being searched for
  626. * @param string $replace The replacement value that replaces found search value
  627. * @param string $subject The string being searched and replaced on
  628. * @return string A string with the replaced value
  629. */
  630. function str_replace_last($search, $replace, $subject)
  631. {
  632. if (!is_array($search)) {
  633. $search = [$search];
  634. }
  635. foreach ($search as $s) {
  636. if ($s !== '' && strrpos($subject, $s) !== false) {
  637. $subject = substr_replace($subject, $replace, strrpos($subject, $s), strlen($s));
  638. }
  639. }
  640. return $subject;
  641. }
  642. }
  643. if (!function_exists('str_starts_with')) {
  644. /**
  645. * Check if $haystack string starts with $needle string.
  646. *
  647. * ### Example:
  648. *
  649. * ```php
  650. * str_starts_with('lorem ipsum', 'lo'); // true
  651. * str_starts_with('lorem ipsum', 'ipsum'); // false
  652. * ```
  653. *
  654. * @param string $haystack The string to search in
  655. * @param string $needle The string to look for
  656. * @return bool
  657. */
  658. function str_starts_with($haystack, $needle)
  659. {
  660. return
  661. $needle === '' ||
  662. strpos($haystack, $needle) === 0;
  663. }
  664. }
  665. if (!function_exists('str_ends_with')) {
  666. /**
  667. * Check if $haystack string ends with $needle string.
  668. *
  669. * ### Example:
  670. *
  671. * ```php
  672. * str_ends_with('lorem ipsum', 'm'); // true
  673. * str_ends_with('dolorem sit amet', 'at'); // false
  674. * ```
  675. *
  676. * @param string $haystack The string to search in
  677. * @param string $needle The string to look for
  678. * @return bool
  679. */
  680. function str_ends_with($haystack, $needle)
  681. {
  682. return
  683. $needle === '' ||
  684. substr($haystack, - strlen($needle)) === $needle;
  685. }
  686. }