/** * Pre-process the given selector to perform any necessary replacements * * This is primarily used to handle sub-selections, i.e. "bar=foo, id=[this=that, foo=bar]" * * @param Selector $selector * @return bool Returns false if selector should be skipped over by getQuery() * */ protected function preProcessSelector(Selector $selector) { if ($selector->quote && !is_array($selector->value) && Selectors::stringHasSelector($selector->value)) { // selector contains an embedded quoted selector if ($selector->quote == '[') { // i.e. field=[id>0, name=something, this=that] // selector contains another embedded selector that we need to convert to page IDs $selectors = new Selectors($selector->value); $hasTemplate = false; $hasParent = false; foreach ($selectors as $s) { if (is_array($s->field)) { continue; } if ($s->field == 'template') { $hasTemplate = true; } if ($s->field == 'parent' || $s->field == 'parent_id') { $hasParent = true; } } // special handling for page references, detect if parent or template is defined, // and add it to the selector if available. This makes it faster. if (!$hasTemplate || !$hasParent) { $fields = is_array($selector->field) ? $selector->field : array($selector->field); $templates = array(); $parents = array(); $findSelector = ''; foreach ($fields as $fieldName) { if (strpos($fieldName, '.') !== false) { list($unused, $fieldName) = explode('.', $fieldName); } $field = $this->wire('fields')->get($fieldName); if (!$field) { continue; } if (!$hasTemplate && $field->template_id) { if (is_array($field->template_id)) { $templates = array_merge($templates, $field->template_id); } else { $templates[] = (int) $field->template_id; } } if (!$hasParent && $field->parent_id) { $parents[] = (int) $field->parent_id; } if ($field->findPagesSelector && count($fields) == 1) { $findSelector = $field->findPagesSelector; } } if (count($templates)) { $selectors->prepend(new SelectorEqual('template', $templates)); } if (count($parents)) { $selectors->prepend(new SelectorEqual('parent_id', $parents)); } if ($findSelector) { foreach (new Selectors($findSelector) as $s) { $selectors->append($s); } } } $pageFinder = new PageFinder(); $ids = $pageFinder->findIDs($selectors); // populate selector value with array of page IDs if (count($ids) == 0) { // subselector resulted in 0 matches // force non-match for this subselector by populating 'id' subfield to field name(s) $fieldNames = array(); foreach ($selector->fields as $key => $fieldName) { if (strpos($fieldName, '.') !== false) { // reduce fieldName to just field name without subfield name list($fieldName, $subname) = explode('.', $fieldName); // subname intentionally unused } $fieldName .= '.id'; $fieldNames[$key] = $fieldName; } $selector->fields = $fieldNames; $selector->value = 0; } else { $selector->value = count($ids) > 1 ? $ids : reset($ids); } $selector->quote = ''; /* } else { $fieldName = $selector->field ? $selector->field : 'none'; if(!isset($this->extraSubSelectors[$fieldName])) $this->extraSubSelectors[$fieldName] = array(); $this->extraSubSelectors[$fieldName][] = new Selectors($selector->value); return false; } */ } else { if ($selector->quote == '(') { // selector contains an quoted selector. At least one () quoted selector must match for each field specified in front of it $fieldName = $selector->field ? $selector->field : 'none'; if (!isset($this->extraOrSelectors[$fieldName])) { $this->extraOrSelectors[$fieldName] = array(); } $this->extraOrSelectors[$fieldName][] = new Selectors($selector->value); return false; } } } return true; }
/** * Hook a function/method to a hookable method call in this object * * Hookable method calls are methods preceded by three underscores. * You may also specify a method that doesn't exist already in the class * The hook method that you define may be part of a class or a globally scoped function. * * If you are hooking a procedural function, you may omit the $toObject and instead just call via: * $this->addHook($method, 'function_name'); or $this->addHook($method, 'function_name', $options); * * @param string $method Method name to hook into, NOT including the three preceding underscores. * May also be Class::Method for same result as using the fromClass option. * @param object|null|callable $toObject Object to call $toMethod from, * Or null if $toMethod is a function outside of an object, * Or function|callable if $toObject is not applicable or function is provided as a closure. * @param string $toMethod Method from $toObject, or function name to call on a hook event. Optional. * @param array $options See self::$defaultHookOptions at the beginning of this class. Optional. * @return string A special Hook ID that should be retained if you need to remove the hook later * @throws WireException * */ public function addHook($method, $toObject, $toMethod = null, $options = array()) { if (is_array($toMethod)) { // $options array specified as 3rd argument if (count($options)) { // combine $options from addHookBefore/After and user specified options $options = array_merge($toMethod, $options); } else { $options = $toMethod; } $toMethod = null; } if (is_null($toMethod)) { // $toObject has been ommitted and a procedural function specified instead // $toObject may also be a closure $toMethod = $toObject; $toObject = null; } if (is_null($toMethod)) { throw new WireException("Method to call is required and was not specified (toMethod)"); } if (substr($method, 0, 3) == '___') { throw new WireException("You must specify hookable methods without the 3 preceding underscores"); } if (method_exists($this, $method)) { throw new WireException("Method " . $this->className() . "::{$method} is not hookable"); } $options = array_merge(self::$defaultHookOptions, $options); if (strpos($method, '::')) { list($fromClass, $method) = explode('::', $method, 2); if (strpos($fromClass, '(') !== false) { // extract object selector match string list($fromClass, $objMatch) = explode('(', $fromClass, 2); $objMatch = trim($objMatch, ') '); if (Selectors::stringHasSelector($objMatch)) { $objMatch = new Selectors($objMatch); } if ($objMatch) { $options['objMatch'] = $objMatch; } } $options['fromClass'] = $fromClass; } $argOpen = strpos($method, '('); if ($argOpen && strpos($method, ')') > $argOpen + 1) { // extract argument selector match string(s), arg 0: Something::something(selector_string) // or: Something::something(1:selector_string, 3:selector_string) matches arg 1 and 3. list($method, $argMatch) = explode('(', $method, 2); $argMatch = trim($argMatch, ') '); if (strpos($argMatch, ':') !== false) { // zero-based argument indexes specified, i.e. 0:template=product, 1:order_status $args = preg_split('/\\b([0-9]):/', trim($argMatch), -1, PREG_SPLIT_DELIM_CAPTURE); if (count($args)) { $argMatch = array(); array_shift($args); // blank while (count($args)) { $argKey = (int) trim(array_shift($args)); $argVal = trim(array_shift($args), ', '); $argMatch[$argKey] = $argVal; } } } else { // just single argument specified, so argument 0 is assumed } if (is_string($argMatch)) { $argMatch = array(0 => $argMatch); } foreach ($argMatch as $argKey => $argVal) { if (Selectors::stringHasSelector($argVal)) { $argMatch[$argKey] = new Selectors($argVal); } } if (count($argMatch)) { $options['argMatch'] = $argMatch; } } if ($options['allInstances'] || $options['fromClass']) { // hook all instances of this class $hookClass = $options['fromClass'] ? $options['fromClass'] : $this->className(); if (!isset(self::$staticHooks[$hookClass])) { self::$staticHooks[$hookClass] = array(); } $hooks =& self::$staticHooks[$hookClass]; $options['allInstances'] = true; $local = 0; } else { // hook only this instance $hookClass = ''; $hooks =& $this->localHooks; $local = 1; } $priority = (string) $options['priority']; if (!isset($hooks[$method])) { if (ctype_digit($priority)) { $priority = "{$priority}.0"; } $hooks[$method] = array(); } else { if (strpos($priority, '.')) { // priority already specifies a sub value: extract it list($priority, $n) = explode('.', $priority); $options['priority'] = $priority; // without $n $priority .= ".{$n}"; } else { $n = 0; $priority .= ".0"; } // come up with a priority that is unique for this class/method across both local and static hooks while ($hookClass && isset(self::$staticHooks[$hookClass][$method][$priority]) || isset($this->localHooks[$method][$priority])) { $n++; $priority = "{$options['priority']}.{$n}"; } } // Note hookClass is always blank when this is a local hook $id = "{$hookClass}:{$priority}:{$method}"; $options['priority'] = $priority; $hooks[$method][$priority] = array('id' => $id, 'method' => $method, 'toObject' => $toObject, 'toMethod' => $toMethod, 'options' => $options); // cacheValue is just the method() or property, cacheKey includes optional fromClass:: $cacheValue = $options['type'] == 'method' ? "{$method}()" : "{$method}"; $cacheKey = ($options['fromClass'] ? $options['fromClass'] . '::' : '') . $cacheValue; self::$hookMethodCache[$cacheKey] = $cacheValue; // keep track of all local hooks combined when debug mode is on if ($this->wire('config')->debug && $hooks === $this->localHooks) { $debugClass = $this->className(); $debugID = ($local ? $debugClass : '') . $id; while (isset(self::$allLocalHooks[$debugID])) { $debugID .= "_"; } $debugHook = $hooks[$method][$priority]; $debugHook['method'] = $debugClass . "->" . $debugHook['method']; self::$allLocalHooks[$debugID] = $debugHook; } // sort by priority, if more than one hook for the method if (count($hooks[$method]) > 1) { defined("SORT_NATURAL") ? ksort($hooks[$method], SORT_NATURAL) : uksort($hooks[$method], "strnatcmp"); } return $id; }
/** * Given a $expire seconds, date, page, or template convert it to an ISO-8601 date * * Returns an array of expires info requires multiple parts, like with self::expireSelector. * In this case it returns array with array('expires' => date, 'selector' => selector); * * @param $expire * @return string|array * */ protected function getExpires($expire) { if (is_object($expire) && $expire->id) { if ($expire instanceof Page) { // page object $expire = $expire->template->id; } else { if ($expire instanceof Template) { // template object $expire = $expire->id; } else { // unknown object, substitute default $expire = time() + self::expireDaily; } } } else { if (is_array($expire)) { // expire value already prepared by a previous call, just return it if (isset($expire['selector']) && isset($expire['expire'])) { return $expire; } } else { if (in_array($expire, array(self::expireNever, self::expireSave))) { // good, we'll take it as-is return $expire; } else { if (is_string($expire) && Selectors::stringHasSelector($expire)) { // expire when page matches selector return array('expire' => self::expireSelector, 'selector' => $expire); } else { // account for date format as string if (is_string($expire) && !ctype_digit("{$expire}")) { $expire = strtotime($expire); $isDate = true; } else { $isDate = false; } if ($expire === 0 || $expire === "0") { // zero is allowed if that's what was specified $expire = (int) $expire; } else { // zero is not allowed because it indicates failed type conversion $expire = (int) $expire; if (!$expire) { $expire = self::expireDaily; } } if ($expire > time()) { // a future date has been specified, so we'll keep it } else { if (!$isDate) { // a quantity of seconds has been specified, add it to current time $expire = time() + $expire; } } } } } } $expire = date(self::dateFormat, $expire); return $expire; }