1<?php
2
3/**
4 * Hoa
5 *
6 *
7 * @license
8 *
9 * New BSD License
10 *
11 * Copyright © 2007-2017, Hoa community. All rights reserved.
12 *
13 * Redistribution and use in source and binary forms, with or without
14 * modification, are permitted provided that the following conditions are met:
15 *     * Redistributions of source code must retain the above copyright
16 *       notice, this list of conditions and the following disclaimer.
17 *     * Redistributions in binary form must reproduce the above copyright
18 *       notice, this list of conditions and the following disclaimer in the
19 *       documentation and/or other materials provided with the distribution.
20 *     * Neither the name of the Hoa nor the names of its contributors may be
21 *       used to endorse or promote products derived from this software without
22 *       specific prior written permission.
23 *
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
28 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
29 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
30 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
31 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
32 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34 * POSSIBILITY OF SUCH DAMAGE.
35 */
36
37namespace Hoa\Zformat;
38
39/**
40 * Class \Hoa\Zformat\Parameter.
41 *
42 * Provide a class parameters support.
43 *
44 * @copyright  Copyright © 2007-2017 Hoa community
45 * @license    New BSD License
46 */
47class Parameter
48{
49    /**
50     * Owner.
51     *
52     * @var string
53     */
54    protected $_owner            = null;
55
56    /**
57     * Parameters.
58     *
59     * @var array
60     */
61    protected $_parameters       = [];
62
63    /**
64     * Keywords.
65     *
66     * @var array
67     */
68    protected $_keywords         = [];
69
70    /**
71     * Constants values for zFormat.
72     *
73     * @var array
74     */
75    protected static $_constants = null;
76
77    /**
78     * Cache for zFormat.
79     *
80     * @var array
81     */
82    protected $_cache            = [];
83
84
85
86    /**
87     * Construct a new set of parameters.
88     *
89     * @param   mixed  $owner         Owner name or instance.
90     * @param   array  $keywords      Keywords.
91     * @param   array  $parameters    Parameters.
92     * @throws  \Hoa\Zformat\Exception
93     */
94    public function __construct(
95        $owner,
96        array $keywords   = [],
97        array $parameters = []
98    ) {
99        if (is_object($owner)) {
100            if (!($owner instanceof Parameterizable)) {
101                throw new Exception(
102                    'Only parameterizable object can have parameter; ' .
103                    '%s does implement \Hoa\Zformat\Parameterizable.',
104                    0,
105                    get_class($owner)
106                );
107            }
108
109            $owner = get_class($owner);
110        } else {
111            $reflection = new \ReflectionClass($owner);
112
113            if (false === $reflection->implementsInterface('\Hoa\Zformat\Parameterizable')) {
114                throw new Exception(
115                    'Only parameterizable object can have parameter; ' .
116                    '%s does implement \Hoa\Zformat\Parameterizable.',
117                    1,
118                    $owner
119                );
120            }
121        }
122
123        $this->_owner = $owner;
124        $this->setKeywords($keywords);
125        $this->setDefault($parameters);
126
127        return;
128    }
129
130    /**
131     * Initialize constants.
132     *
133     * @return  void
134     */
135    public static function initializeConstants()
136    {
137        $c                = explode('…', date('d…j…N…w…z…W…m…n…Y…y…g…G…h…H…i…s…u…O…T…U'));
138        self::$_constants = [
139            'd' => $c[0],
140            'j' => $c[1],
141            'N' => $c[2],
142            'w' => $c[3],
143            'z' => $c[4],
144            'W' => $c[5],
145            'm' => $c[6],
146            'n' => $c[7],
147            'Y' => $c[8],
148            'y' => $c[9],
149            'g' => $c[10],
150            'G' => $c[11],
151            'h' => $c[12],
152            'H' => $c[13],
153            'i' => $c[14],
154            's' => $c[15],
155            'u' => $c[16],
156            'O' => $c[17],
157            'T' => $c[18],
158            'U' => $c[19]
159        ];
160
161        return;
162    }
163
164    /**
165     * Get constants.
166     *
167     * @return  array
168     */
169    public static function getConstants()
170    {
171        return self::$_constants;
172    }
173
174    /**
175     * Set default parameters to a class.
176     *
177     * @param   array  $parameters    Parameters to set.
178     * @return  void
179     * @throws  \Hoa\Zformat\Exception
180     */
181    private function setDefault(array $parameters)
182    {
183        $this->_parameters = $parameters;
184
185        return;
186    }
187
188    /**
189     * Set parameters.
190     *
191     * @param   array   $parameters    Parameters.
192     * @return  void
193     */
194    public function setParameters(array $parameters)
195    {
196        $this->resetCache();
197
198        foreach ($parameters as $key => $value) {
199            $this->setParameter($key, $value);
200        }
201
202        return;
203    }
204
205    /**
206     * Get parameters.
207     *
208     * @return  array
209     */
210    public function getParameters()
211    {
212        return $this->_parameters;
213    }
214
215    /**
216     * Set a parameter.
217     *
218     * @param   string  $key      Key.
219     * @param   mixed   $value    Value.
220     * @return  mixed
221     */
222    public function setParameter($key, $value)
223    {
224        $this->resetCache();
225        $old = null;
226
227        if (true === array_key_exists($key, $this->_parameters)) {
228            $old = $this->_parameters[$key];
229        }
230
231        $this->_parameters[$key] = $value;
232
233        return $old;
234    }
235
236    /**
237     * Get a parameter.
238     *
239     * @param   string  $parameter    Parameter.
240     * @return  mixed
241     */
242    public function getParameter($parameter)
243    {
244        if (array_key_exists($parameter, $this->_parameters)) {
245            return $this->_parameters[$parameter];
246        }
247
248        return null;
249    }
250
251    /**
252     * Get a formatted parameter (i.e. zFormatted).
253     *
254     * @param   string  $parameter    Parameter.
255     * @return  mixed
256     */
257    public function getFormattedParameter($parameter)
258    {
259        if (null === $value = $this->getParameter($parameter)) {
260            return null;
261        }
262
263        return $this->zFormat($value);
264    }
265
266    /**
267     * Check a branch exists.
268     *
269     * @param   string  $branch    Branch.
270     * @return  bool
271     */
272    public function branchExists($branch)
273    {
274        $qBranch = preg_quote($branch);
275
276        foreach ($this->getParameters() as $key => $value) {
277            if (0 !== preg_match('#^' . $qBranch . '(.*)?#', $key)) {
278                return true;
279            }
280        }
281
282        return false;
283    }
284
285    /**
286     * Unlinearize a branch to an array.
287     *
288     * @param   string  $branch    Branch.
289     * @return  array
290     */
291    public function unlinearizeBranch($branch)
292    {
293        $parameters = $this->getParameters();
294        $out        = [];
295        $lBranch    = strlen($branch);
296
297        foreach ($parameters as $key => $value) {
298            if ($branch !== substr($key, 0, $lBranch)) {
299                continue;
300            }
301
302            $handle  = [];
303            $explode = preg_split(
304                '#((?<!\\\)\.)#',
305                substr($key, $lBranch + 1),
306                -1,
307                PREG_SPLIT_NO_EMPTY
308            );
309            $end = count($explode) - 1;
310            $i   = $end;
311
312            while ($i >= 0) {
313                $explode[$i] = str_replace('\\.', '.', $explode[$i]);
314
315                if ($i != $end) {
316                    $handle = [$explode[$i] => $handle];
317                } else {
318                    $handle = [$explode[$i] => $this->zFormat($value)];
319                }
320
321                --$i;
322            }
323
324            $out = array_merge_recursive($out, $handle);
325        }
326
327        return $out;
328    }
329
330    /**
331     * Set keywords.
332     *
333     * @param   array   $keywords    Keywords.
334     * @return  void
335     * @throws  \Hoa\Zformat\Exception
336     */
337    public function setKeywords($keywords)
338    {
339        $this->resetCache();
340
341        foreach ($keywords as $key => $value) {
342            $this->setKeyword($key, $value);
343        }
344
345        return;
346    }
347
348    /**
349     * Get keywords.
350     *
351     * @return  array
352     */
353    public function getKeywords()
354    {
355        return $this->_keywords;
356    }
357
358    /**
359     * Set a keyword.
360     *
361     * @param   string  $key      Key.
362     * @param   mixed   $value    Value.
363     * @return  mixed
364     */
365    public function setKeyword($key, $value)
366    {
367        $this->resetCache();
368        $old = null;
369
370        if (true === array_key_exists($key, $this->_keywords)) {
371            $old = $this->_keywords[$key];
372        }
373
374        $this->_keywords[$key] = $value;
375
376        return $old;
377    }
378
379    /**
380     * Get a keyword.
381     *
382     * @param   string  $keyword    Keyword.
383     * @return  mixed
384     */
385    public function getKeyword($keyword)
386    {
387        if (true === array_key_exists($keyword, $this->_keywords)) {
388            return $this->_keywords[$keyword];
389        }
390
391        return null;
392    }
393
394    /**
395     * zFormat a string.
396     * zFormat is inspired from the famous Zsh (please, take a look at
397     * http://zsh.org), and specifically from ZStyle.
398     *
399     * ZFormat has the following pattern:
400     *     (:subject[:format]:)
401     *
402     * where subject could be a:
403     *   • keyword, i.e. a simple string: foo;
404     *   • reference to an existing parameter, i.e. a simple string prefixed by
405     *     a %: %bar;
406     *   • constant, i.e. a combination of chars, first is prefixed by a _: _Ymd
407     *     will given the current year, followed by the current month and
408     *     finally the current day.
409     *
410     * and where the format is a combination of chars, that apply functions on
411     * the subject:
412     *   • h: to get the head of a path (equivalent to dirname);
413     *   • t: to get the tail of a path (equivalent to basename);
414     *   • r: to get the path without extension;
415     *   • e: to get the extension;
416     *   • l: to get the result in lowercase;
417     *   • u: to get the result in uppercase;
418     *   • U: to get the result with the first letter in uppercase (understand
419     *        classname);
420     *   • s/<foo>/<bar>/: to replace all matches <foo> by <bar> (the last / is
421     *     optional, only if more options are given after);
422     *   • s%<foo>%<bar>%: to replace the prefix <foo> by <bar> (the last % is
423     *     also optional);
424     *   • s#<foo>#<bar>#: to replace the suffix <foo> by <bar> (the last # is
425     *     also optional).
426     *
427     * Known constants are:
428     *   • d: day of the month, 2 digits with leading zeros;
429     *   • j: day of the month without leading zeros;
430     *   • N: ISO-8601 numeric representation of the day of the week;
431     *   • w: numeric representation of the day of the week;
432     *   • z: the day of the year (starting from 0);
433     *   • W: ISO-8601 week number of year, weeks starting on Monday;
434     *   • m: numeric representation of a month, with leading zeros;
435     *   • n: numeric representation of a month, without leading zeros;
436     *   • Y: a full numeric representation of a year, 4 digits;
437     *   • y: a two digit representation of a year;
438     *   • g: 12-hour format of an hour without leading zeros;
439     *   • G: 24-hour format of an hour without leading zeros;
440     *   • h: 12-hour format of an hour with leading zeros;
441     *   • H: 24-hour format of an hour with leading zeros;
442     *   • i: minutes with leading zeros;
443     *   • s: seconds with leading zeros;
444     *   • u: microseconds;
445     *   • O: difference to Greenwich time (GMT) in hours;
446     *   • T: timezone abbreviation;
447     *   • U: seconds since the Unix Epoch (a timestamp).
448     * They are very useful for dynamic cache paths for example.
449     *
450     * Examples:
451     *   Let keywords $k and parameters $p:
452     *     $k = [
453     *         'foo'      => 'bar',
454     *         'car'      => 'DeLoReAN',
455     *         'power'    => 2.21,
456     *         'answerTo' => 'life_universe_everything_else',
457     *         'answerIs' => 42,
458     *         'hello'    => 'wor.l.d'
459     *     ];
460     *     $p = [
461     *         'plpl'        => '(:foo:U:)',
462     *         'foo'         => 'ar(:%plpl:)',
463     *         'favoriteCar' => 'A (:car:l:)!',
464     *         'truth'       => 'To (:answerTo:ls/_/ /U:) is (:answerIs:).',
465     *         'file'        => '/a/file/(:_Ymd:)/(:hello:trr:).(:power:e:)',
466     *         'recursion'   => 'oof(:%foo:s#ar#az:)'
467     *     ];
468     *   Then, after applying the zFormat, we get:
469     *     • plpl:        'Bar', put the first letter in uppercase;
470     *     • foo:         'arBar', call the parameter plpl;
471     *     • favoriteCar: 'A delorean!', all is in lowercase;
472     *     • truth:       'To Life universe everything else is 42', all is in
473     *                    lowercase, then replace underscores by spaces, and
474     *                    finally put the first letter in uppercase; and no
475     *                    transformation for 42;
476     *     • file:        '/a/file/20090505/wor.21', get date constants, then
477     *                    get the tail of the path and remove extension twice,
478     *                    and add the extension of power;
479     *     • recursion:   'oofarBaz', get 'arbar' first, and then, replace the
480     *                    suffix 'ar' by 'az'.
481     *
482     * @param   string  $value    Parameter value.
483     * @return  string
484     * @throws  \Hoa\Zformat\Exception
485     */
486    public function zFormat($value)
487    {
488        if (!is_string($value)) {
489            return $value;
490        }
491
492        if (isset($this->_cache[$value])) {
493            return $this->_cache[$value];
494        }
495
496        if (null === self::$_constants) {
497            self::initializeConstants();
498        }
499
500        $self       = $this;
501        $keywords   = $this->getKeywords();
502        $parameters = $this->getParameters();
503
504        return $this->_cache[$value] = preg_replace_callback(
505            '#\(:(.*?):\)#',
506            function ($match) use ($self, $value, &$keywords, &$parameters) {
507                preg_match(
508                    '#([^:]+)(?::(.*))?#',
509                    $match[1],
510                    $submatch
511                );
512
513                if (!isset($submatch[1])) {
514                    return '';
515                }
516
517                $out  = null;
518                $key  = $submatch[1];
519                $word = substr($key, 1);
520
521                // Call a parameter.
522                if ('%' == $key[0]) {
523                    if (false === array_key_exists($word, $parameters)) {
524                        throw new Exception(
525                            'Parameter %s is not found in parameters.',
526                            0,
527                            $word
528                        );
529                    }
530
531                    $handle = $parameters[$word];
532                    $out    = $self->zFormat($handle);
533                }
534                // Call a constant.
535                elseif ('_' == $key[0]) {
536                    $constants = Parameter::getConstants();
537
538                    foreach (str_split($word) as $k => $v) {
539                        if (!isset($constants[$v])) {
540                            throw new Exception(
541                                'Constant char %s is not supported in the ' .
542                                'rule %s.',
543                                1,
544                                [$v, $value]
545                            );
546                        }
547
548                        $out .= $constants[$v];
549                    }
550                }
551                // Call a keyword.
552                else {
553                    if (false === array_key_exists($key, $keywords)) {
554                        throw new Exception(
555                            'Keyword %s is not found in the rule %s.',
556                            2,
557                            [$key, $value]
558                        );
559                    }
560
561                    $out = $keywords[$key];
562                }
563
564                if (!isset($submatch[2])) {
565                    return $out;
566                }
567
568                preg_match_all(
569                    '#(h|t|r|e|l|u|U|s(/|%|\#)(.*?)(?<!\\\)\2(.*?)(?:(?<!\\\)\2|$))#',
570                    $submatch[2],
571                    $flags
572                );
573
574                if (empty($flags) || empty($flags[1])) {
575                    throw new Exception(
576                        'Unrecognized format pattern %s in the rule %s.',
577                        3,
578                        [$match[0], $value]
579                    );
580                }
581
582                foreach ($flags[1] as $i => $flag) {
583                    switch ($flag) {
584                        case 'h':
585                            $out = dirname($out);
586
587                            break;
588
589                        case 't':
590                            $out = basename($out);
591
592                            break;
593
594                        case 'r':
595                            if (false !== $position = strrpos($out, '.', 1)) {
596                                $out = substr($out, 0, $position);
597                            }
598
599                            break;
600
601                        case 'e':
602                            if (false !== $position = strrpos($out, '.', 1)) {
603                                $out = substr($out, $position + 1);
604                            }
605
606                            break;
607
608                        case 'l':
609                            $out = strtolower($out);
610
611                            break;
612
613                        case 'u':
614                            $out = strtoupper($out);
615
616                            break;
617
618                        case 'U':
619                            $handle = null;
620
621                            foreach (explode('\\', $out) as $part) {
622                                if (null === $handle) {
623                                    $handle  = ucfirst($part);
624                                } else {
625                                    $handle .= '\\' . ucfirst($part);
626                                }
627                            }
628
629                            $out = $handle;
630
631                            break;
632
633                        default:
634                            if (!isset($flags[3]) && !isset($flags[4])) {
635                                throw new Exception(
636                                    'Unrecognized format pattern in the rule %s.',
637                                    4,
638                                    $value
639                                );
640                            }
641
642                            $l = preg_quote($flags[3][$i], '#');
643                            $r = $flags[4][$i];
644
645                            switch ($flags[2][$i]) {
646                                case '%':
647                                    $l  = '^' . $l;
648
649                                    break;
650
651                                case '#':
652                                    $l .= '$';
653
654                                    break;
655                            }
656
657                            $out = preg_replace('#' . $l . '#', $r, $out);
658                    }
659                }
660
661                return $out;
662            },
663            $value
664        );
665    }
666
667    /**
668     * Reset zFormat cache.
669     *
670     * @return  void
671     */
672    private function resetCache()
673    {
674        unset($this->_cache);
675        $this->_cache = [];
676
677        return;
678    }
679}
680