/ */ class Site_Model_PullPathGroup extends P4Cms_Model { const ONLY_CONFLICTS = 'onlyConflicts'; const RECURSIVE = 'recursive'; protected static $_fields = array( 'label' => array( 'accessor' => 'getLabel', 'mutator' => 'setLabel' ), 'basePaths' => array( 'accessor' => 'getBasePaths', 'mutator' => 'setBasePaths', 'default' => array() ), 'paths' => array( 'accessor' => 'getPaths', 'mutator' => 'setPaths' ), 'parent' => array( 'accessor' => 'getParent', 'mutator' => 'setParent' ), 'subGroups' => array( 'accessor' => 'getSubGroups', 'mutator' => 'setSubGroups' ), 'count' => array( 'accessor' => 'getCount', 'mutator' => 'setCount' ), 'details' => array( 'accessor' => 'getDetails', 'mutator' => 'setDetails', ) ); /** * Set the label for this path group. * * @param string|null $label the human-friendly label for this group. * @return Site_Model_PullPathGroup provides fluent interface. */ public function setLabel($label) { return $this->_setValue('label', $label); } /** * Get the label for this path group. * * @return string|null the human-friendly label for this group. */ public function getLabel() { return $this->_getValue('label'); } /** * Set a callback which will provide details about the paths * held in this group. The callback will be passed an iterator * of Paths (output of getPaths) and is expected to return an * iterator of Models. The keys/values in the models will inform * the columns used to display your data. At a minimum a 'label', * 'conflict' and 'action' value should be present. * * If no callback is set the default details will be returned. * See getDetails for more information. * * @param callable|null $callback the callback to use * @return Site_Model_PullPathGroup to maintain a fluent interface * @throws InvalidArgumentException if an invalid type is passed */ public function setDetails($callback) { if (!is_callable($callback) && !is_null($callback)) { throw new InvalidArgumentException( 'Details Callback must be callable or null' ); } return $this->_setValue('details', $callback); } /** * Provides details about the paths held in this group. * Details are provided as an iterator of Models. The keys/values in * the models are intended to represent columns of data. At a minimum * a 'label', 'conflict' and 'action' value should be present on each * model. * * The returned iterator may optionally use a 'columns' property * to provide column id's (e.g. label) with custom titles. If no * columns property is present the default columns of Label and Action * will be used. * * If a details callback is present it will be utilized to generate * the details. Otherwise, each path will get default details using * the depotFile as a label and existing branch action and conflict * status. * * @param string|array|null $options options to influence the result: * ONLY_CONFLICTS - if passed, only conflicting * path details are returned * RECURSIVE - if passed, details of all sub * groups paths will be included * @return P4Cms_Model_Iterator the path details */ public function getDetails($options = null) { $options = (array) $options; $onlyConflicts = in_array(static::ONLY_CONFLICTS, $options) ? static::ONLY_CONFLICTS : null; $paths = $this->getPaths($onlyConflicts); $callback = $this->_getValue('details'); $details = new P4Cms_Model_Iterator; if (is_callable($callback) && $paths->count()) { $details = $callback($paths) ?: new P4Cms_Model_Iterator; foreach ($details as $detail) { if (!$detail->hasField("type")) { $detail->setValue('type', $this->getLabel()); } } } else if ($paths->count()) { foreach ($paths as $path) { $details[] = new P4Cms_Model( array( 'action' => $path->action, 'conflict' => $path->conflict, 'type' => $this->getLabel(), 'label' => preg_replace('#^//[^/]+/[^/]+/#', '', $path->depotFile) ) ); } $details->setProperty('columns', array('label' => 'File', 'action' => 'Action')); } $columns = $details->hasProperty('columns') ? $details->getProperty('columns') : array(); if (in_array(static::RECURSIVE, (array) $options)) { foreach ($this->getSubGroups() as $group) { $subDetails = $group->getDetails($options); if ($subDetails->count()) { if ($subDetails->hasProperty('columns')) { $columns += $subDetails->getProperty('columns'); } $details->merge($subDetails); } } } // ensure our details iterator has the aggregated columns // fall back to defaults of Label and Action if none found. $details->setProperty( 'columns', $columns ?: array('label' => 'Label', 'action' => 'Action') ); return $details; } /** * Returns the details callback without actually executing it. * * @return callable|null the details callback or null if none set */ public function getDetailsCallback() { return $this->_getValue('details'); } /** * Add the specified base path to this path group. * * Base path(s) can be set to optimize pull operations * (as they will be used instead of paths). * * @param string $basePath the basepath to add * @return Site_Model_PullPathGroup to maintain a fluent interface */ public function addBasePath($basePath) { $basePaths = $this->getBasePaths(); $basePaths[] = $basePath; return $this->_setValue('basePaths', $basePaths); } /** * Add the passed array of base paths to this path group. * * Base path(s) can be set to optimize pull operations * (as they will be used instead of paths). * * @param array $basePaths the basepaths to add * @return Site_Model_PullPathGroup to maintain a fluent interface */ public function addBasePaths(array $basePaths) { foreach ($basePaths as $path) { $this->addBasePath($path); } return $this; } /** * Set the base path(s) for this path group. * * Base path(s) can be set to optimize pull operations * (as they will be used instead of paths). * * @param array|string|null $basePaths the base-path(s) for files in this group. * @return Site_Model_PullPathGroup provides fluent interface. */ public function setBasePaths($basePaths) { $this->_setValue('basePaths', array()); $basePaths = (array) $basePaths; foreach ($basePaths as $basePath) { $this->addBasePath($basePath); } return $this; } /** * Get the base paths for this path group. * * @return array the base-paths for files in this group. */ public function getBasePaths() { return $this->_getValue('basePaths'); } /** * Set the parent group for this path group. * * This gives sub-groups access to their parent. * If this group has basepaths set on it, specifiying a parent will * trigger moving any matching paths from parent group(s). * * @param Site_Model_PullPathGroup|null $parent parent group or null. * @return Site_Model_PullPathGroup provides fluent interface. */ public function setParent(Site_Model_PullPathGroup $parent = null) { $this->_setValue('parent', $parent); if ($parent) { $this->setBasePaths($this->getBasePaths()); } return $this; } /** * Get the parent group for this path group (if one is set) * * @return Site_Model_PullPathGroup|null parent group or null. */ public function getParent() { return $this->_getValue('parent'); } /** * Control the count of paths in this path group. * * By default simply returns a count of all paths and paths in sub-groups * (recursively). Can be set to null for the default behaviour or a callback * that may use whatever logic is desired to return a count. * * @param null|callable $count null to clear the count (fallback to default), or * a callback that returns the count (passed $this, * the computed count, $recursive and $onlyConflicts) * @return Site_Model_PullPathGroup provides fluent interface. */ public function setCount($count) { if (!is_null($count) && !is_callable($count)) { throw InvalidArgumentException( "Cannot set count. Given count is not a valid type." ); } return $this->_setValue('count', $count); } /** * Get the count of paths in this path group. * This will execute the count callback and use its result if one is set. * * @param string|array|null $options options to influence the result: * ONLY_CONFLICTS - if passed, only conflicting * paths are counted * RECURSIVE - if passed, count of all sub * groups will be included * @return int the number of paths in this path group. * this is not always an exact count of paths * as a fixed value or a callback can be used. */ public function getCount($options = null) { $options = (array) $options; $onlyConflicts = in_array(static::ONLY_CONFLICTS, $options) ? static::ONLY_CONFLICTS : null; $computed = $this->getPaths($onlyConflicts)->count(); // if a count callback has been set, use it, // passing this and the count we computed above $count = $this->_getValue('count'); if (is_callable($count)) { // just pass the 'conflictsOnly' option to the callback. // we handle recursion ourselves. $computed = call_user_func($count, $this, $computed, (array) $onlyConflicts); } // compute the number of paths recursively if needed. if (in_array(static::RECURSIVE, $options)) { foreach ($this->getSubGroups() as $group) { $computed += $group->getCount($options); } } return $computed; } /** * Returns the count callback without actually executing it. * * @return callable|null the count callback or null if none set */ public function getCountCallback() { return $this->_getValue('count'); } /** * A convienence method that calls through to getCount * returning a shallow count (non-recursive) of conflicts. * * @return int The number of conflicting entries (shallow) */ public function getConflictCount() { return $this->getCount(Site_Model_PullPathGroup::ONLY_CONFLICTS); } /** * Set the paths in this path group. * * @param P4Cms_Model_Iterator|null $paths the list of paths to put * in this group (null to clear). * @return Site_Model_PullPathGroup provides fluent interface. */ public function setPaths(P4Cms_Model_Iterator $paths = null) { $paths = $paths ?: new P4Cms_Model_Iterator; return $this->_setValue('paths', $paths); } /** * Get the paths in this path group (not recursive). * * @param string|array|null $options options to influence the result: * ONLY_CONFLICTS - if passed, only conflicting * paths are returned * RECURSIVE - if passed, paths of all sub * groups will be included * @return P4Cms_Model_Iterator an iterator of paths in this group. */ public function getPaths($options = null) { // initialize paths to an iterator if necessary. $paths = $this->_getValue('paths') ?: new P4Cms_Model_Iterator; $this->_setValue('paths', $paths); if (in_array(static::ONLY_CONFLICTS, (array) $options)) { $paths = $paths->filter('conflict', true, array(P4Cms_Model_Iterator::FILTER_COPY)); } if (in_array(static::RECURSIVE, (array) $options)) { foreach ($this->getSubGroups() as $group) { $paths->merge($group->getPaths($options)); } } return $paths; } /** * Add a path to this path group. The path must be an array of * information about the path (containing at least a depotFile) * or a model. If an array is given it will be normalized to a * model. * * @param array|P4Cms_Model $path the path array or model to add. * @return Site_Model_PullPathGroup provides fluent interface. */ public function addPath($path) { if (!is_array($path) && !$path instanceof P4Cms_Model) { throw new InvalidArgumentArgument("Cannot set path. Path must be an array or model"); } if (is_array($path)) { $path = new P4Cms_Model($path); } $paths = $this->getPaths(); $paths[] = $path; return $this; } /** * Set the sub-groups in this path group. * * @param P4Cms_Model_Iterator|null $groups the sub-groups to put in * this group (null to clear). * @return Site_Model_PullPathGroup provides fluent interface. */ public function setSubGroups(P4Cms_Model_Iterator $groups = null) { $groups = $groups ?: new P4Cms_Model_Iterator; foreach ($groups as $group) { $this->addSubGroup($group); } return $this->_setValue('subGroups', $groups); } /** * Add a sub-group to this path group. The sub-group must be an * array of information about the group or pull path group model. * If an array is given it will be normalized to a model. * * This method will set the parent of the given sub-group to be * this group. * * If the sub-group is passed as an array with a 'inheritPaths' * property any matching paths will automatically move from this * group (or its parents) to the new sub-group. * * @param array|Site_Model_PullPathGroup $group the group array or model to add. * @return Site_Model_PullPathGroup provides fluent interface. */ public function addSubGroup($group) { if (!is_array($group) && !$group instanceof Site_Model_PullPathGroup) { throw new InvalidArgumentException( "Cannot add sub-group. Group must be a array or path group object." ); } $inheritPaths = array(); if (is_array($group)) { if (isset($group['inheritPaths'])) { $inheritPaths = $group['inheritPaths']; unset($group['inheritPaths']); } $group = new Site_Model_PullPathGroup($group); } // ensure the sub-group has a link back to this (its parent) $group->setParent($this); $group->inheritPaths($inheritPaths); $groups = $this->getSubGroups(); $groups[] = $group; return $this; } /** * Get the immediate child groups of this group (not recursive). * * @return P4Cms_Model_Iterator a list of child groups. */ public function getSubGroups() { // initialize sub-groups to an iterator if necessary. $groups = $this->_getValue('subGroups') ?: new P4Cms_Model_Iterator; $this->_setValue('subGroups', $groups); return $groups; } /** * Try to find a sub-group with the given label under this group. * * @param string $label the name of the sub-group to look for. * @return Site_Model_PullPathGroup|bool a matching sub-group or false if no such group. */ public function getSubGroup($label) { return $this->getSubGroups() ->filter('label', $label, array(P4Cms_Model_Iterator::FILTER_COPY)) ->first(); } /** * Find a group under this group (includes this group) with the given id. * If no matching group can be found, returns false. * * @param string $id the id of the group to look for. * @param Site_Model_PullPathGroup|null $group used for recursion. * @return Site_Model_PullPathGroup|bool the found group or false. */ public function findById($id, $group = null) { $group = $group ?: $this; // check for immediate match. if ($group->getId() === $id) { return $group; } // check for recursive (sub-group) match. foreach ($group->getSubGroups() as $subGroup) { $found = $this->findById($id, $subGroup); if ($found) { return $found; } } return false; } /** * Get an identifier for this group. * * If an explicit id is set, it will be returned. Otherwise, we * generate an identifier for this path group based on its label * and the labels of its parents (e.g. 'configuration/permissions') * * Note: this identifier is not guaranteed to be unique! * * @return string an identifier for this group. */ public function getId() { // if an explicit id was set, use it. if (parent::getId()) { return parent::getId(); } $ids = array(); $filter = new P4Cms_Filter_TitleToId; $parent = $this; while ($parent) { $ids[] = $filter->filter($parent->getLabel()); $parent = $parent->getParent(); } return trim(implode('/', array_reverse($ids)), '/'); } /** * Get a list of paths in this group to use in a pull operation. * * If base path(s) are set, these will be returned (as an optimization). * Otherwise, a list of depotFiles will be taken from getPaths and returned. * * @return array paths in this group to use in a pull operation */ public function getIncludePaths() { return $this->getBasePaths() ?: $this->getPaths()->invoke('getValue', array('depotFile')); } /** * Scans up through parent objects and brings over any paths matching * the passed filespec(s). If the passed filespec ends in the wildcard * '...' anything starting with the value will be considered a match. * * If a path model is passed its 'depotFile' value will be used as the * search value and any matching paths brought over. * * @param array|Iterator|string|P4Cms_Model $paths one or more paths to inherit * @return Site_Model_PullPathGroup to maintain a fluent interface */ public function inheritPaths($paths) { // we need to selectively 'arrayize' input if only a single // path was passed. blindly casting to array would 'toArray' // P4Cms_Model's causing a later invalid argument exception. if (!$paths instanceof Iterator && !is_array($paths)) { $paths = array($paths); } // if paths are in an iterator the cursor may become corrupt // if we take a path from a parent with a shared reference. // swap it over to being an array of paths for safety. if ($paths instanceof Iterator) { $paths = iterator_to_array($paths); } foreach ($paths as $path) { // normalize to a string if ($path instanceof P4Cms_Model && $path->depotFile) { $path = $path->depotFile; } if (!$path || !is_string($path)) { throw new InvalidArgumentException( 'Inherit path must be a non empty string or path model.' ); } // our 'needle' starts out as the full path value requiring // an exact match. if the path ends in '...' we switch to a // starts with match and strip the trailing wildcard. $needle = $path; $mode = 'exact'; if (substr($needle, -3) == '...') { $mode = 'starts'; $needle = substr($needle, 0, -3); } $parent = $this->getParent(); while ($parent) { $paths = $parent->getPaths(); foreach ($paths->toArray(true) as $key => $path) { // if we have an acceptable match, add the path to our group // and remove it from the parent that originally had it. if (($mode == 'exact' && $needle === $path->getValue('depotFile')) || ($mode == 'starts' && strpos($path->getValue('depotFile'), $needle) === 0) ) { $this->addPath($path); unset($paths[$key]); } } // up the tree. $parent = $parent->getParent(); } } } /** * Helper function to fetch records from either the source or the target * branch as necessary. First attempts to fetch from the source branch * and falls back to the target branch if any of the identified records * could not be found in the source. * * When doing a 'copy' style of pull, we can get into a situation where * items that only exist in the target are opened for delete. Naturally, * these records can't be fetched from the source, so we fetch them from * the target branch instead. * * If a given id cannot be found in either the source or the target branch * it will not be included in the result. * * @param array $ids a list of ids to fetch * @param string $class the type of record to fetch. * @param P4Cms_Site $source the source site/branch to fetch from. * @param P4Cms_Site $target the target site/branch to fallback to. * @return P4Cms_Model_Iterator fetched records. */ public static function fetchRecords(array $ids, $class, P4Cms_Site $source, P4Cms_Site $target) { if (!class_exists($class) || (!is_subclass_of($class, 'P4Cms_Record') && $class !== 'P4Cms_Record')) { throw new InvalidArgumentException("Cannot fetch entries. Invalid class type specified."); } // nothing to do if no ids given. if (empty($ids)) { return new P4Cms_Model_Iterator; } // first attempt to fetch from the source branch. $entries = $class::fetchAll( array( 'ids' => $ids, 'includeDeleted' => true ), $source->getStorageAdapter() ); // if we got everything, all done! $missing = array_diff($ids, $entries->invoke('getId')); if (!$missing) { return $entries; } // try to fetch missing records from the target branch. return $entries->merge( $class::fetchAll( array( 'ids' => $missing, 'includeDeleted' => true ), $target->getStorageAdapter() ) ); } /** * Helper function to re-index a given paths iterator by record id. * Any paths that are not of the given record class type (ie. not * under the appropriate storage path) will be excluded. * * @param P4Cms_Model_Iterator $paths an iterator of paths to re-index. * @param string $class the type of record to use to convert paths to ids. * @param P4Cms_Record_Adapter|null $adapter optional - a specific storage adapter to use. * @return P4Cms_Model_Iterator iterator of paths of the given record type indexed by id. */ public static function pathsByRecordId(P4Cms_Model_Iterator $paths, $class, P4Cms_Record_Adapter $adapter = null) { $byId = new P4Cms_Model_Iterator; foreach ($paths as $path) { try { $id = $class::depotFileToId($path->depotFile, $adapter); $byId[$id] = $path; } catch (P4Cms_Record_Exception $e) { // skip entries whose id can't be determined (assumed to be of a different type). } } return $byId; } }