1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\View\Helper; 11 12use stdClass; 13use Zend\View; 14use Zend\View\Exception; 15 16/** 17 * Zend\View\Helper\HeadMeta 18 * 19 * @see http://www.w3.org/TR/xhtml1/dtds.html 20 * 21 * Allows the following 'virtual' methods: 22 * @method HeadMeta appendName($keyValue, $content, $modifiers = array()) 23 * @method HeadMeta offsetGetName($index, $keyValue, $content, $modifiers = array()) 24 * @method HeadMeta prependName($keyValue, $content, $modifiers = array()) 25 * @method HeadMeta setName($keyValue, $content, $modifiers = array()) 26 * @method HeadMeta appendHttpEquiv($keyValue, $content, $modifiers = array()) 27 * @method HeadMeta offsetGetHttpEquiv($index, $keyValue, $content, $modifiers = array()) 28 * @method HeadMeta prependHttpEquiv($keyValue, $content, $modifiers = array()) 29 * @method HeadMeta setHttpEquiv($keyValue, $content, $modifiers = array()) 30 * @method HeadMeta appendProperty($keyValue, $content, $modifiers = array()) 31 * @method HeadMeta offsetGetProperty($index, $keyValue, $content, $modifiers = array()) 32 * @method HeadMeta prependProperty($keyValue, $content, $modifiers = array()) 33 * @method HeadMeta setProperty($keyValue, $content, $modifiers = array()) 34 */ 35class HeadMeta extends Placeholder\Container\AbstractStandalone 36{ 37 /** 38 * Allowed key types 39 * 40 * @var array 41 */ 42 protected $typeKeys = array('name', 'http-equiv', 'charset', 'property', 'itemprop'); 43 44 /** 45 * Required attributes for meta tag 46 * 47 * @var array 48 */ 49 protected $requiredKeys = array('content'); 50 51 /** 52 * Allowed modifier keys 53 * 54 * @var array 55 */ 56 protected $modifierKeys = array('lang', 'scheme'); 57 58 /** 59 * Registry key for placeholder 60 * 61 * @var string 62 */ 63 protected $regKey = 'Zend_View_Helper_HeadMeta'; 64 65 /** 66 * Constructor 67 * 68 * Set separator to PHP_EOL 69 * 70 */ 71 public function __construct() 72 { 73 parent::__construct(); 74 75 $this->setSeparator(PHP_EOL); 76 } 77 78 /** 79 * Retrieve object instance; optionally add meta tag 80 * 81 * @param string $content 82 * @param string $keyValue 83 * @param string $keyType 84 * @param array $modifiers 85 * @param string $placement 86 * @return HeadMeta 87 */ 88 public function __invoke( 89 $content = null, 90 $keyValue = null, 91 $keyType = 'name', 92 $modifiers = array(), 93 $placement = Placeholder\Container\AbstractContainer::APPEND 94 ) { 95 if ((null !== $content) && (null !== $keyValue)) { 96 $item = $this->createData($keyType, $keyValue, $content, $modifiers); 97 $action = strtolower($placement); 98 switch ($action) { 99 case 'append': 100 case 'prepend': 101 case 'set': 102 $this->$action($item); 103 break; 104 default: 105 $this->append($item); 106 break; 107 } 108 } 109 110 return $this; 111 } 112 113 /** 114 * Overload method access 115 * 116 * @param string $method 117 * @param array $args 118 * @throws Exception\BadMethodCallException 119 * @return HeadMeta 120 */ 121 public function __call($method, $args) 122 { 123 if (preg_match('/^(?P<action>set|(pre|ap)pend|offsetSet)(?P<type>Name|HttpEquiv|Property|Itemprop)$/', $method, $matches)) { 124 $action = $matches['action']; 125 $type = $this->normalizeType($matches['type']); 126 $argc = count($args); 127 $index = null; 128 129 if ('offsetSet' == $action) { 130 if (0 < $argc) { 131 $index = array_shift($args); 132 --$argc; 133 } 134 } 135 136 if (2 > $argc) { 137 throw new Exception\BadMethodCallException( 138 'Too few arguments provided; requires key value, and content' 139 ); 140 } 141 142 if (3 > $argc) { 143 $args[] = array(); 144 } 145 146 $item = $this->createData($type, $args[0], $args[1], $args[2]); 147 148 if ('offsetSet' == $action) { 149 return $this->offsetSet($index, $item); 150 } 151 152 $this->$action($item); 153 154 return $this; 155 } 156 157 return parent::__call($method, $args); 158 } 159 160 /** 161 * Render placeholder as string 162 * 163 * @param string|int $indent 164 * @return string 165 */ 166 public function toString($indent = null) 167 { 168 $indent = (null !== $indent) 169 ? $this->getWhitespace($indent) 170 : $this->getIndent(); 171 172 $items = array(); 173 $this->getContainer()->ksort(); 174 175 try { 176 foreach ($this as $item) { 177 $items[] = $this->itemToString($item); 178 } 179 } catch (Exception\InvalidArgumentException $e) { 180 trigger_error($e->getMessage(), E_USER_WARNING); 181 return ''; 182 } 183 184 return $indent . implode($this->escape($this->getSeparator()) . $indent, $items); 185 } 186 187 /** 188 * Create data item for inserting into stack 189 * 190 * @param string $type 191 * @param string $typeValue 192 * @param string $content 193 * @param array $modifiers 194 * @return stdClass 195 */ 196 public function createData($type, $typeValue, $content, array $modifiers) 197 { 198 $data = new stdClass; 199 $data->type = $type; 200 $data->$type = $typeValue; 201 $data->content = $content; 202 $data->modifiers = $modifiers; 203 204 return $data; 205 } 206 207 /** 208 * Build meta HTML string 209 * 210 * @param stdClass $item 211 * @throws Exception\InvalidArgumentException 212 * @return string 213 */ 214 public function itemToString(stdClass $item) 215 { 216 if (!in_array($item->type, $this->typeKeys)) { 217 throw new Exception\InvalidArgumentException(sprintf( 218 'Invalid type "%s" provided for meta', 219 $item->type 220 )); 221 } 222 $type = $item->type; 223 224 $modifiersString = ''; 225 foreach ($item->modifiers as $key => $value) { 226 if ($this->view->plugin('doctype')->isHtml5() 227 && $key == 'scheme' 228 ) { 229 throw new Exception\InvalidArgumentException( 230 'Invalid modifier "scheme" provided; not supported by HTML5' 231 ); 232 } 233 if (!in_array($key, $this->modifierKeys)) { 234 continue; 235 } 236 $modifiersString .= $key . '="' . $this->escape($value) . '" '; 237 } 238 239 $modifiersString = rtrim($modifiersString); 240 241 if ('' != $modifiersString) { 242 $modifiersString = ' ' . $modifiersString; 243 } 244 245 if (method_exists($this->view, 'plugin')) { 246 if ($this->view->plugin('doctype')->isHtml5() 247 && $type == 'charset' 248 ) { 249 $tpl = ($this->view->plugin('doctype')->isXhtml()) 250 ? '<meta %s="%s"/>' 251 : '<meta %s="%s">'; 252 } elseif ($this->view->plugin('doctype')->isXhtml()) { 253 $tpl = '<meta %s="%s" content="%s"%s />'; 254 } else { 255 $tpl = '<meta %s="%s" content="%s"%s>'; 256 } 257 } else { 258 $tpl = '<meta %s="%s" content="%s"%s />'; 259 } 260 261 $meta = sprintf( 262 $tpl, 263 $type, 264 $this->escape($item->$type), 265 $this->escape($item->content), 266 $modifiersString 267 ); 268 269 if (isset($item->modifiers['conditional']) 270 && !empty($item->modifiers['conditional']) 271 && is_string($item->modifiers['conditional']) 272 ) { 273 // inner wrap with comment end and start if !IE 274 if (str_replace(' ', '', $item->modifiers['conditional']) === '!IE') { 275 $meta = '<!-->' . $meta . '<!--'; 276 } 277 $meta = '<!--[if ' . $this->escape($item->modifiers['conditional']) . ']>' . $meta . '<![endif]-->'; 278 } 279 280 return $meta; 281 } 282 283 /** 284 * Normalize type attribute of meta 285 * 286 * @param string $type type in CamelCase 287 * @throws Exception\DomainException 288 * @return string 289 */ 290 protected function normalizeType($type) 291 { 292 switch ($type) { 293 case 'Name': 294 return 'name'; 295 case 'HttpEquiv': 296 return 'http-equiv'; 297 case 'Property': 298 return 'property'; 299 case 'Itemprop': 300 return 'itemprop'; 301 default: 302 throw new Exception\DomainException(sprintf( 303 'Invalid type "%s" passed to normalizeType', 304 $type 305 )); 306 } 307 } 308 309 /** 310 * Determine if item is valid 311 * 312 * @param mixed $item 313 * @return bool 314 */ 315 protected function isValid($item) 316 { 317 if ((!$item instanceof stdClass) 318 || !isset($item->type) 319 || !isset($item->modifiers) 320 ) { 321 return false; 322 } 323 324 if (!isset($item->content) 325 && (! $this->view->plugin('doctype')->isHtml5() 326 || (! $this->view->plugin('doctype')->isHtml5() && $item->type !== 'charset')) 327 ) { 328 return false; 329 } 330 331 // <meta itemprop= ... /> is only supported with doctype html 332 if (! $this->view->plugin('doctype')->isHtml5() 333 && $item->type === 'itemprop' 334 ) { 335 return false; 336 } 337 338 // <meta property= ... /> is only supported with doctype RDFa 339 if (!$this->view->plugin('doctype')->isRdfa() 340 && $item->type === 'property' 341 ) { 342 return false; 343 } 344 345 return true; 346 } 347 348 /** 349 * Append 350 * 351 * @param string $value 352 * @return void 353 * @throws Exception\InvalidArgumentException 354 */ 355 public function append($value) 356 { 357 if (!$this->isValid($value)) { 358 throw new Exception\InvalidArgumentException( 359 'Invalid value passed to append; please use appendMeta()' 360 ); 361 } 362 363 return $this->getContainer()->append($value); 364 } 365 366 /** 367 * OffsetSet 368 * 369 * @param string|int $index 370 * @param string $value 371 * @throws Exception\InvalidArgumentException 372 * @return void 373 */ 374 public function offsetSet($index, $value) 375 { 376 if (!$this->isValid($value)) { 377 throw new Exception\InvalidArgumentException( 378 'Invalid value passed to offsetSet; please use offsetSetName() or offsetSetHttpEquiv()' 379 ); 380 } 381 382 return $this->getContainer()->offsetSet($index, $value); 383 } 384 385 /** 386 * OffsetUnset 387 * 388 * @param string|int $index 389 * @throws Exception\InvalidArgumentException 390 * @return void 391 */ 392 public function offsetUnset($index) 393 { 394 if (!in_array($index, $this->getContainer()->getKeys())) { 395 throw new Exception\InvalidArgumentException('Invalid index passed to offsetUnset()'); 396 } 397 398 return $this->getContainer()->offsetUnset($index); 399 } 400 401 /** 402 * Prepend 403 * 404 * @param string $value 405 * @throws Exception\InvalidArgumentException 406 * @return void 407 */ 408 public function prepend($value) 409 { 410 if (!$this->isValid($value)) { 411 throw new Exception\InvalidArgumentException( 412 'Invalid value passed to prepend; please use prependMeta()' 413 ); 414 } 415 416 return $this->getContainer()->prepend($value); 417 } 418 419 /** 420 * Set 421 * 422 * @param string $value 423 * @throws Exception\InvalidArgumentException 424 * @return void 425 */ 426 public function set($value) 427 { 428 if (!$this->isValid($value)) { 429 throw new Exception\InvalidArgumentException('Invalid value passed to set; please use setMeta()'); 430 } 431 432 $container = $this->getContainer(); 433 foreach ($container->getArrayCopy() as $index => $item) { 434 if ($item->type == $value->type && $item->{$item->type} == $value->{$value->type}) { 435 $this->offsetUnset($index); 436 } 437 } 438 439 return $this->append($value); 440 } 441 442 /** 443 * Create an HTML5-style meta charset tag. Something like <meta charset="utf-8"> 444 * 445 * Not valid in a non-HTML5 doctype 446 * 447 * @param string $charset 448 * @return HeadMeta Provides a fluent interface 449 */ 450 public function setCharset($charset) 451 { 452 $item = new stdClass; 453 $item->type = 'charset'; 454 $item->charset = $charset; 455 $item->content = null; 456 $item->modifiers = array(); 457 $this->set($item); 458 459 return $this; 460 } 461} 462