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