Advertisement
p4geoff

Untitled

Mar 26th, 2014
104
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. <?php
  2. /**
  3.  * This class layers support for plural specs such as changes, jobs,
  4.  * users, etc. on top of the singular spec support already present
  5.  * in P4\Spec\SingularAbstract.
  6.  *
  7.  * @copyright   2011 Perforce Software. All rights reserved.
  8.  * @license     Please see LICENSE.txt in top-level folder of this distribution.
  9.  * @version     <release>/<patch>
  10.  */
  11.  
  12. namespace P4\Spec;
  13.  
  14. use P4;
  15. use P4\Validate;
  16. use P4\Spec\Exception\Exception;
  17. use P4\Spec\Exception\NotFoundException;
  18. use P4\Connection\ConnectionInterface;
  19. use P4\Model\Fielded\Iterator as FieldedIterator;
  20. use P4\OutputHandler\Limit;
  21.  
  22. abstract class PluralAbstract extends SingularAbstract
  23. {
  24.     const ID_FIELD              = null;
  25.     const FETCH_MAXIMUM         = 'maximum';
  26.     const FETCH_AFTER           = 'after';
  27.     const TEMP_ID_PREFIX        = '~tmp';
  28.     const TEMP_ID_DELIMITER     = ".";
  29.  
  30.     /**
  31.      * Get the id of this spec entry.
  32.      *
  33.      * @return  null|string     the id of this entry.
  34.      */
  35.     public function getId()
  36.     {
  37.         if (array_key_exists(static::ID_FIELD, $this->values)) {
  38.             return $this->values[static::ID_FIELD];
  39.         } else {
  40.             return null;
  41.         }
  42.     }
  43.  
  44.     /**
  45.      * Set the id of this spec entry. Id must be in a valid format or null.
  46.      *
  47.      * @param   null|string     $id     the id of this entry - pass null to clear.
  48.      * @return  PluralAbstract          provides a fluent interface
  49.      * @throws  \InvalidArgumentException   if id does not pass validation.
  50.      */
  51.     public function setId($id)
  52.     {
  53.         if ($id !== null && !static::isValidId($id)) {
  54.             throw new \InvalidArgumentException("Cannot set id. Id is invalid.");
  55.         }
  56.  
  57.         // if populate was deferred, caller expects it
  58.         // to have been populated already.
  59.         $this->populate();
  60.  
  61.         $this->values[static::ID_FIELD] = $id;
  62.  
  63.         return $this;
  64.     }
  65.  
  66.     /**
  67.      * Determine if a spec record with the given id exists.
  68.      * Must be implemented by sub-classes because this test
  69.      * is impractical to generalize.
  70.      *
  71.      * @param   string                  $id             the id to check for.
  72.      * @param   ConnectionInterface     $connection     optional - a specific connection to use.
  73.      * @return  bool    true if the given id matches an existing record.
  74.      */
  75.     abstract public static function exists($id, ConnectionInterface $connection = null);
  76.  
  77.     /**
  78.      * Get the requested spec entry from Perforce.
  79.      *
  80.      * @param   string                  $id         the id of the entry to fetch.
  81.      * @param   ConnectionInterface     $connection optional - a specific connection to use.
  82.      * @return  PluralAbstract          instace of the requested entry.
  83.      * @throws  \InvalidArgumentException   if no id is given.
  84.      */
  85.     public static function fetch($id, ConnectionInterface $connection = null)
  86.     {
  87.         // ensure a valid id is provided.
  88.         if (!static::isValidId($id)) {
  89.             throw new \InvalidArgumentException("Must supply a valid id to fetch.");
  90.         }
  91.  
  92.         // if no connection given, use default.
  93.         $connection = $connection ?: static::getDefaultConnection();
  94.  
  95.         // ensure id exists.
  96.         if (!static::exists($id, $connection)) {
  97.             throw new NotFoundException(
  98.                 "Cannot fetch " . static::SPEC_TYPE . " $id. Record does not exist."
  99.             );
  100.         }
  101.  
  102.         // construct spec instance.
  103.         $spec = new static($connection);
  104.         $spec->setId($id)
  105.              ->deferPopulate();
  106.  
  107.         return $spec;
  108.     }
  109.  
  110.     /**
  111.      * Get all entries of this type from Perforce.
  112.      *
  113.      * @param   array   $options    optional - array of options to augment fetch behavior.
  114.      *                              supported options are:
  115.      *
  116.      *                                  FETCH_MAXIMUM - set to integer value to limit to the
  117.      *                                                  first 'max' number of entries.
  118.      *                                    FETCH_AFTER - set to an id _after_ which to start collecting entries
  119.      *                                                  note: entries seen before 'after' count towards max.
  120.      *
  121.      * @param   ConnectionInterface     $connection optional - a specific connection to use.
  122.      * @return  FieldedIterator         all records of this type.
  123.      * @todo    make limit work for depot (in a P4\Spec\Depot sub-class)
  124.      */
  125.     public static function fetchAll($options = array(), ConnectionInterface $connection = null)
  126.     {
  127.         // if no connection given, use default.
  128.         $connection = $connection ?: static::getDefaultConnection();
  129.  
  130.         // get command to use
  131.         $command = static::getFetchAllCommand();
  132.  
  133.         // get command flags for given fetch options.
  134.         $flags = static::getFetchAllFlags($options);
  135.  
  136.         // fetch all specs.
  137.         // configure a handler to enforce 'after' (skip entries up to and including 'after')
  138.         $after = isset($options[static::FETCH_AFTER]) ? $options[static::FETCH_AFTER] : null;
  139.         if (strlen($after)) {
  140.             $idField = static::ID_FIELD;
  141.             $isAfter = false;
  142.             $handler = new Limit;
  143.             $handler->setFilterCallback(
  144.                 function ($data) use ($after, $idField, &$isAfter) {
  145.                     if ($after && !$isAfter) {
  146.                         // id field could be upper or lower case in list output.
  147.                         $id      = isset($data[lcfirst($idField)]) ? $data[lcfirst($idField)] : null;
  148.                         $id      = !$id && isset($data[$idField])  ? $data[$idField]          : $id;
  149.                         $isAfter = ($after == $id);
  150.                         return false;
  151.                     }
  152.                     return true;
  153.                 }
  154.             );
  155.             $result = $connection->runHandler($handler, $command, $flags);
  156.         } else {
  157.             $result = $connection->run($command, $flags);
  158.         }
  159.  
  160.         // expand any sequences present
  161.         $result->expandSequences();
  162.  
  163.         // convert result data to spec objects.
  164.         $specs = new FieldedIterator;
  165.         foreach ($result->getData() as $data) {
  166.             $spec = static::fromSpecListEntry($data, $flags, $connection);
  167.             $specs[$spec->getId()] = $spec;
  168.         }
  169.  
  170.         return $specs;
  171.     }
  172.  
  173.     /**
  174.      * Create a temporary entry.
  175.      *
  176.      * The passed values can, optionally, specify the id of the temp entry.
  177.      * If no id is passed in values, one will be generated following the
  178.      * conventions described in makeTempId().
  179.      *
  180.      * Temp entries are deleted when the connection is closed.
  181.      *
  182.      * @param   array|null              $values             optional - values to set on temp entry,
  183.      *                                                      can include ID
  184.      * @param   function|null           $cleanupCallback    optional - callback to use for cleanup.
  185.      *                                                      signature is:
  186.      *                                                      function($entry, $defaultCallback)
  187.      * @param   ConnectionInterface     $connection optional - a specific connection to use.
  188.      * @return  PluralAbstract          instace of the temp entry.
  189.      */
  190.     public static function makeTemp(
  191.         array $values = null,
  192.         $cleanupCallback = null,
  193.         ConnectionInterface $connection = null
  194.     ) {
  195.         // normalize to array
  196.         $values = $values ?: array();
  197.  
  198.         // generate an id if no value for our id field is present
  199.         if (!isset($values[static::ID_FIELD])) {
  200.             $values[static::ID_FIELD] = static::makeTempId();
  201.         }
  202.  
  203.         // create the temporary instance.
  204.         $temp = new static($connection);
  205.         $temp->set($values)->save();
  206.  
  207.         // remove the temp entry when the connection terminates.
  208.         $defaultCallback = static::getTempCleanupCallback();
  209.         $temp->getConnection()->addDisconnectCallback(
  210.             function ($connection) use ($temp, $cleanupCallback, $defaultCallback) {
  211.                 try {
  212.                     // use the passed callback if valid, fallback to the default callback
  213.                     if (is_callable($cleanupCallback)) {
  214.                         $cleanupCallback($temp, $defaultCallback);
  215.                     } else {
  216.                         $defaultCallback($temp);
  217.                     }
  218.                 } catch (\Exception $e) {
  219.                     P4\Log::logException("Failed to delete temporary entry.", $e);
  220.                 }
  221.             }
  222.         );
  223.  
  224.         return $temp;
  225.     }
  226.  
  227.     /**
  228.      * Generate a temporary id by combining the id prefix
  229.      * with the current time, pid and a random uniqid():
  230.      *
  231.      *  ~tmp.<unixtime>.<pid>.<uniqid>
  232.      *
  233.      * The leading tilde ('~') places the temporary id at the end of
  234.      * the list.  The unixtime ensures that the oldest ids will
  235.      * appear first (among temp ids), while the pid and uniqid provide
  236.      * reasonable assurance that no two ids will collide.
  237.      *
  238.      * @return  string  an id suitable for use with temporary specs.
  239.      */
  240.     public static function makeTempId()
  241.     {
  242.         return implode(
  243.             static::TEMP_ID_DELIMITER,
  244.             array(
  245.                 static::TEMP_ID_PREFIX,
  246.                 time(),
  247.                 getmypid(),
  248.                 uniqid("", true)
  249.             )
  250.         );
  251.     }
  252.  
  253.     /**
  254.      * Delete this spec entry.
  255.      *
  256.      * @param   array   $params     optional - additional flags to pass to delete
  257.      *                              (e.g. some specs support -f to force delete).
  258.      * @return  PluralAbstract      provides a fluent interface
  259.      * @throws  Exception           if no id has been set.
  260.      */
  261.     public function delete(array $params = null)
  262.     {
  263.         $id = $this->getId();
  264.         if ($id === null) {
  265.             throw new Exception("Cannot delete. No id has been set.");
  266.         }
  267.  
  268.         // ensure id exists.
  269.         $connection = $this->getConnection();
  270.         if (!static::exists($id, $connection)) {
  271.             throw new NotFoundException(
  272.                 "Cannot delete " . static::SPEC_TYPE . " $id. Record does not exist."
  273.             );
  274.         }
  275.  
  276.         $params = array_merge((array) $params, array("-d", $id));
  277.         $result = $connection->run(static::SPEC_TYPE, $params);
  278.  
  279.         // should re-populate.
  280.         $this->deferPopulate(true);
  281.  
  282.         return $this;
  283.     }
  284.  
  285.     /**
  286.      * Get a field's raw value.
  287.      * Extend parent to use getId() for id field.
  288.      *
  289.      * @param   string      $field  the name of the field to get the value of.
  290.      * @return  mixed       the value of the field.
  291.      * @throws  Exception   if the field does not exist.
  292.      */
  293.     public function getRawValue($field)
  294.     {
  295.         if ($field === static::ID_FIELD) {
  296.             return $this->getId();
  297.         }
  298.  
  299.         // call-through.
  300.         return parent::getRawValue($field);
  301.     }
  302.  
  303.     /**
  304.      * Set a field's raw value.
  305.      * Extend parent to use setId() for id field.
  306.      *
  307.      * @param   string  $field      the name of the field to set the value of.
  308.      * @param   mixed   $value      the value to set in the field.
  309.      * @return  SingularAbstract    provides a fluent interface
  310.      * @throws  Exception           if the field does not exist.
  311.      */
  312.     public function setRawValue($field, $value)
  313.     {
  314.         if ($field === static::ID_FIELD) {
  315.             return $this->setId($value);
  316.         }
  317.  
  318.         // call-through.
  319.         return parent::setRawValue($field, $value);
  320.     }
  321.  
  322.     /**
  323.      * Extended to preserve id when values are cleared.
  324.      * Schedule populate to run when data is requested (lazy-load).
  325.      *
  326.      * @param   bool    $reset  optionally clear instance values.
  327.      */
  328.     public function deferPopulate($reset = false)
  329.     {
  330.         if ($reset) {
  331.             $id = $this->getId();
  332.         }
  333.  
  334.         parent::deferPopulate($reset);
  335.  
  336.         if ($reset) {
  337.             $this->setId($id);
  338.         }
  339.     }
  340.  
  341.     /**
  342.      * Provide a callback function to be used during cleanup of
  343.      * temp entries. The callback should expect a single parameter,
  344.      * the entry being removed.
  345.      *
  346.      * @return callable     A callback function with the signature function($entry)
  347.      */
  348.     protected static function getTempCleanupCallback()
  349.     {
  350.         return function ($entry) {
  351.             // remove the temp entry we are responsible for
  352.             $entry->delete();
  353.         };
  354.     }
  355.  
  356.     /**
  357.      * Check if the given id is in a valid format for this spec type.
  358.      *
  359.      * @param   string      $id     the id to check
  360.      * @return  bool        true if id is valid, false otherwise
  361.      */
  362.     protected static function isValidId($id)
  363.     {
  364.         $validator = new Validate\SpecName;
  365.         return $validator->isValid($id);
  366.     }
  367.  
  368.     /**
  369.      * Extend parent populate to exit early if id is null.
  370.      */
  371.     protected function populate()
  372.     {
  373.         // early exit if populate not needed.
  374.         if (!$this->needsPopulate) {
  375.             return;
  376.         }
  377.  
  378.         // don't attempt populate if id null.
  379.         if ($this->getId() === null) {
  380.             return;
  381.         }
  382.  
  383.         parent::populate();
  384.     }
  385.  
  386.     /**
  387.      * Get raw spec data direct from Perforce. No caching involved.
  388.      * Extends parent to supply an id to the spec -o command.
  389.      *
  390.      * @return  array   $data   the raw spec output from Perforce.
  391.      */
  392.     protected function getSpecData()
  393.     {
  394.         $result = $this->getConnection()->run(
  395.             static::SPEC_TYPE,
  396.             array("-o", $this->getId())
  397.         );
  398.         return $result->expandSequences()->getData(-1);
  399.     }
  400.  
  401.     /**
  402.      * Given a spec entry from spec list output (e.g. 'p4 jobs'), produce
  403.      * an instance of this spec with field values set where possible.
  404.      *
  405.      * @param   array                   $listEntry      a single spec entry from spec list output.
  406.      * @param   array                   $flags          the flags that were used for this 'fetchAll' run.
  407.      * @param   ConnectionInterface     $connection     a specific connection to use.
  408.      * @return  PluralAbstract          a (partially) populated instance of this spec class.
  409.      */
  410.     protected static function fromSpecListEntry($listEntry, $flags, ConnectionInterface $connection)
  411.     {
  412.         // most spec list entries have leading lower-case field
  413.         // names which is inconsistent with defined field names.
  414.         // make all field names lead with an upper-case letter.
  415.         $keys      = array_map('ucfirst', array_keys($listEntry));
  416.         $listEntry = array_combine($keys, $listEntry);
  417.  
  418.         // convert common timestamps to dates
  419.         if (isset($listEntry['Time'])) {
  420.             $listEntry['Date']   = static::timeToDate($listEntry['Time'],   $connection);
  421.             unset($listEntry['Time']);
  422.         }
  423.         if (isset($listEntry['Update'])) {
  424.             $listEntry['Update'] = static::timeToDate($listEntry['Update'], $connection);
  425.             unset($listEntry['Update']);
  426.         }
  427.         if (isset($listEntry['Access'])) {
  428.             $listEntry['Access'] = static::timeToDate($listEntry['Access'], $connection);
  429.             unset($listEntry['Access']);
  430.         }
  431.  
  432.         // instantiate new spec object and set raw field values.
  433.         $spec = new static($connection);
  434.         $spec->setRawValues($listEntry)
  435.              ->deferPopulate();
  436.  
  437.         return $spec;
  438.     }
  439.  
  440.     /**
  441.      * Convert the given unix timestamp into the server's typical date
  442.      * format accounting for the server's current timezone.
  443.      *
  444.      * @param   int|string          $time       the timestamp to convert
  445.      * @param   ConnectionInterface $connection the connection to use
  446.      * @return  string              date in the typical server format
  447.      */
  448.     protected static function timeToDate($time, ConnectionInterface $connection)
  449.     {
  450.         $date = new \DateTime('@' . $time);
  451.  
  452.         // try and use the p4 info timezone, if that fails fall back to our local timezone
  453.         try {
  454.             $date->setTimeZone($connection->getTimeZone());
  455.         } catch (\Exception $e) {
  456.             // we tried and failed; just let it use php's default time zone
  457.             // note when creating a DateTime from a unix timestamp the timezone will
  458.             // be UTC, we need to explicitly set it to the default time zone.
  459.             $date->setTimeZone(new \DateTimeZone(date_default_timezone_get()));
  460.         }
  461.  
  462.         return $date->format('Y/m/d H:i:s');
  463.     }
  464.  
  465.     /**
  466.      * Inverse function to timeToDate(), it converts the given date in server's typical
  467.      * format into a unix timestamp accounting for the server's current timezone.
  468.      *
  469.      * @param  string               $date           date in typical server's format (Y/m/d H:i:s) to convert
  470.      * @param  ConnectionInterface  $connection     the connection to use
  471.      * @return int|false            date in unix timestamp or false if unable to convert
  472.      */
  473.     protected static function dateToTime($date, ConnectionInterface $connection)
  474.     {
  475.         // try and use the p4 info timezone, if that fails fall back to our local timezone
  476.         $dateTimeZone = null;
  477.         try {
  478.             $dateTimeZone = $connection->getTimeZone();
  479.         } catch (\Exception $e) {
  480.             // we tried and failed; just let it use php's default time zone
  481.         }
  482.  
  483.         $dateTime = $dateTimeZone
  484.             ? \DateTime::createFromFormat('Y/m/d H:i:s', $date, $dateTimeZone)
  485.             : \DateTime::createFromFormat('Y/m/d H:i:s', $date);
  486.  
  487.         return $dateTime ? (int) $dateTime->format('U') : false;
  488.     }
  489.  
  490.     /**
  491.      * Produce set of flags for the spec list command, given fetch all options array.
  492.      *
  493.      * @param   array   $options    array of options to augment fetch behavior.
  494.      *                              see fetchAll for documented options.
  495.      * @return  array   set of flags suitable for passing to spec list command.
  496.      */
  497.     protected static function getFetchAllFlags($options)
  498.     {
  499.         $flags = array();
  500.  
  501.         if (isset($options[self::FETCH_MAXIMUM])) {
  502.             $flags[] = "-m";
  503.             $flags[] = (int) $options[self::FETCH_MAXIMUM];
  504.         }
  505.  
  506.         return $flags;
  507.     }
  508.  
  509.     /**
  510.      * Get the fetch all command, generally a plural version of the spec type.
  511.      *
  512.      * @return  string  Perforce command to use for fetchAll
  513.      */
  514.     protected static function getFetchAllCommand()
  515.     {
  516.         // derive list command from spec type by adding 's'
  517.         // this works for most of the known plural specs
  518.         return static::SPEC_TYPE . "s";
  519.     }
  520. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement