[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/celerity/ -> CelerityResourceMapGenerator.php (source)

   1  <?php
   2  
   3  final class CelerityResourceMapGenerator {
   4  
   5    private $debug = false;
   6    private $resources;
   7  
   8    private $nameMap     = array();
   9    private $symbolMap   = array();
  10    private $requiresMap = array();
  11    private $packageMap  = array();
  12  
  13    public function __construct(CelerityPhysicalResources $resources) {
  14      $this->resources = $resources;
  15    }
  16  
  17    public function getNameMap() {
  18      return $this->nameMap;
  19    }
  20  
  21    public function getSymbolMap() {
  22      return $this->symbolMap;
  23    }
  24  
  25    public function getRequiresMap() {
  26      return $this->requiresMap;
  27    }
  28  
  29    public function getPackageMap() {
  30      return $this->packageMap;
  31    }
  32  
  33    public function setDebug($debug) {
  34      $this->debug = $debug;
  35      return $this;
  36    }
  37  
  38    protected function log($message) {
  39      if ($this->debug) {
  40        $console = PhutilConsole::getConsole();
  41        $console->writeErr("%s\n", $message);
  42      }
  43    }
  44  
  45    public function generate() {
  46      $binary_map = $this->rebuildBinaryResources($this->resources);
  47  
  48      $this->log(pht('Found %d binary resources.', count($binary_map)));
  49  
  50      $xformer = id(new CelerityResourceTransformer())
  51        ->setMinify(false)
  52        ->setRawURIMap(ipull($binary_map, 'uri'));
  53  
  54      $text_map = $this->rebuildTextResources($this->resources, $xformer);
  55  
  56      $this->log(pht('Found %d text resources.', count($text_map)));
  57  
  58      $resource_graph = array();
  59      $requires_map = array();
  60      $symbol_map = array();
  61      foreach ($text_map as $name => $info) {
  62        if (isset($info['provides'])) {
  63          $symbol_map[$info['provides']] = $info['hash'];
  64  
  65          // We only need to check for cycles and add this to the requires map
  66          // if it actually requires anything.
  67          if (!empty($info['requires'])) {
  68            $resource_graph[$info['provides']] = $info['requires'];
  69            $requires_map[$info['hash']] = $info['requires'];
  70          }
  71        }
  72      }
  73  
  74      $this->detectGraphCycles($resource_graph);
  75      $name_map = ipull($binary_map, 'hash') + ipull($text_map, 'hash');
  76      $hash_map = array_flip($name_map);
  77  
  78      $package_map = $this->rebuildPackages(
  79        $this->resources,
  80        $symbol_map,
  81        $hash_map);
  82  
  83      $this->log(pht('Found %d packages.', count($package_map)));
  84  
  85      $component_map = array();
  86      foreach ($package_map as $package_name => $package_info) {
  87        foreach ($package_info['symbols'] as $symbol) {
  88          $component_map[$symbol] = $package_name;
  89        }
  90      }
  91  
  92      $name_map = $this->mergeNameMaps(
  93        array(
  94          array(pht('Binary'), ipull($binary_map, 'hash')),
  95          array(pht('Text'), ipull($text_map, 'hash')),
  96          array(pht('Package'), ipull($package_map, 'hash')),
  97        ));
  98      $package_map = ipull($package_map, 'symbols');
  99  
 100      ksort($name_map);
 101      ksort($symbol_map);
 102      ksort($requires_map);
 103      ksort($package_map);
 104  
 105      $this->nameMap     = $name_map;
 106      $this->symbolMap   = $symbol_map;
 107      $this->requiresMap = $requires_map;
 108      $this->packageMap  = $package_map;
 109  
 110      return $this;
 111    }
 112  
 113    public function write() {
 114      $map_content = $this->formatMapContent(array(
 115        'names'    => $this->getNameMap(),
 116        'symbols'  => $this->getSymbolMap(),
 117        'requires' => $this->getRequiresMap(),
 118        'packages' => $this->getPackageMap(),
 119      ));
 120  
 121      $map_path = $this->resources->getPathToMap();
 122      $this->log(pht('Writing map "%s".', Filesystem::readablePath($map_path)));
 123      Filesystem::writeFile($map_path, $map_content);
 124  
 125      return $this;
 126    }
 127  
 128    private function formatMapContent(array $data) {
 129      $content = phutil_var_export($data);
 130      $generated = '@'.'generated';
 131  
 132      return <<<EOFILE
 133  <?php
 134  
 135  /**
 136   * This file is automatically generated. Use 'bin/celerity map' to rebuild it.
 137   *
 138   * {$generated}
 139   */
 140  return {$content};
 141  
 142  EOFILE;
 143    }
 144  
 145    /**
 146     * Find binary resources (like PNG and SWF) and return information about
 147     * them.
 148     *
 149     * @param CelerityPhysicalResources Resource map to find binary resources for.
 150     * @return map<string, map<string, string>> Resource information map.
 151     */
 152    private function rebuildBinaryResources(
 153      CelerityPhysicalResources $resources) {
 154  
 155      $binary_map = $resources->findBinaryResources();
 156      $result_map = array();
 157  
 158      foreach ($binary_map as $name => $data_hash) {
 159        $hash = $resources->getCelerityHash($data_hash.$name);
 160  
 161        $result_map[$name] = array(
 162          'hash' => $hash,
 163          'uri'  => $resources->getResourceURI($hash, $name),
 164        );
 165      }
 166  
 167      return $result_map;
 168    }
 169  
 170    /**
 171     * Find text resources (like JS and CSS) and return information about them.
 172     *
 173     * @param CelerityPhysicalResources Resource map to find text resources for.
 174     * @param CelerityResourceTransformer Configured resource transformer.
 175     * @return map<string, map<string, string>> Resource information map.
 176     */
 177    private function rebuildTextResources(
 178      CelerityPhysicalResources $resources,
 179      CelerityResourceTransformer $xformer) {
 180  
 181      $text_map = $resources->findTextResources();
 182      $result_map = array();
 183  
 184      foreach ($text_map as $name => $data_hash) {
 185        $raw_data = $resources->getResourceData($name);
 186        $xformed_data = $xformer->transformResource($name, $raw_data);
 187  
 188        $data_hash = $resources->getCelerityHash($xformed_data);
 189        $hash = $resources->getCelerityHash($data_hash.$name);
 190  
 191        list($provides, $requires) = $this->getProvidesAndRequires(
 192          $name,
 193          $raw_data);
 194  
 195        $result_map[$name] = array(
 196          'hash' => $hash,
 197        );
 198  
 199        if ($provides !== null) {
 200          $result_map[$name] += array(
 201            'provides' => $provides,
 202            'requires' => $requires,
 203          );
 204        }
 205      }
 206  
 207      return $result_map;
 208    }
 209  
 210    /**
 211     * Parse the `@provides` and `@requires` symbols out of a text resource, like
 212     * JS or CSS.
 213     *
 214     * @param string Resource name.
 215     * @param string Resource data.
 216     * @return pair<string|null, list<string>|null> The `@provides` symbol and
 217     *    the list of `@requires` symbols. If the resource is not part of the
 218     *    dependency graph, both are null.
 219     */
 220    private function getProvidesAndRequires($name, $data) {
 221      $parser = new PhutilDocblockParser();
 222  
 223      $matches = array();
 224      $ok = preg_match('@/[*][*].*?[*]/@s', $data, $matches);
 225      if (!$ok) {
 226        throw new Exception(
 227          pht(
 228            'Resource "%s" does not have a header doc comment. Encode '.
 229            'dependency data in a header docblock.',
 230            $name));
 231      }
 232  
 233      list($description, $metadata) = $parser->parse($matches[0]);
 234  
 235      $provides = preg_split('/\s+/', trim(idx($metadata, 'provides')));
 236      $requires = preg_split('/\s+/', trim(idx($metadata, 'requires')));
 237      $provides = array_filter($provides);
 238      $requires = array_filter($requires);
 239  
 240      if (!$provides) {
 241        // Tests and documentation-only JS is permitted to @provide no targets.
 242        return array(null, null);
 243      }
 244  
 245      if (count($provides) > 1) {
 246        throw new Exception(
 247          pht('Resource "%s" must @provide at most one Celerity target.', $name));
 248      }
 249  
 250      return array(head($provides), $requires);
 251    }
 252  
 253    /**
 254     * Check for dependency cycles in the resource graph. Raises an exception if
 255     * a cycle is detected.
 256     *
 257     * @param map<string, list<string>> Map of `@provides` symbols to their
 258     *                                  `@requires` symbols.
 259     * @return void
 260     */
 261    private function detectGraphCycles(array $nodes) {
 262      $graph = id(new CelerityResourceGraph())
 263        ->addNodes($nodes)
 264        ->setResourceGraph($nodes)
 265        ->loadGraph();
 266  
 267      foreach ($nodes as $provides => $requires) {
 268        $cycle = $graph->detectCycles($provides);
 269        if ($cycle) {
 270          throw new Exception(
 271            pht('Cycle detected in resource graph: %s', implode(' > ', $cycle)));
 272        }
 273      }
 274    }
 275  
 276    /**
 277     * Build package specifications for a given resource source.
 278     *
 279     * @param CelerityPhysicalResources Resource source to rebuild.
 280     * @param map<string, string> Map of `@provides` to hashes.
 281     * @param map<string, string> Map of hashes to resource names.
 282     * @return map<string, map<string, string>> Package information maps.
 283     */
 284    private function rebuildPackages(
 285      CelerityPhysicalResources $resources,
 286      array $symbol_map,
 287      array $reverse_map) {
 288  
 289      $package_map = array();
 290  
 291      $package_spec = $resources->getResourcePackages();
 292      foreach ($package_spec as $package_name => $package_symbols) {
 293        $type = null;
 294        $hashes = array();
 295        foreach ($package_symbols as $symbol) {
 296          $symbol_hash = idx($symbol_map, $symbol);
 297          if ($symbol_hash === null) {
 298            throw new Exception(
 299              pht(
 300                'Package specification for "%s" includes "%s", but that symbol '.
 301                'is not @provided by any resource.',
 302                $package_name,
 303                $symbol));
 304          }
 305  
 306          $resource_name = $reverse_map[$symbol_hash];
 307          $resource_type = $resources->getResourceType($resource_name);
 308          if ($type === null) {
 309            $type = $resource_type;
 310          } else if ($type !== $resource_type) {
 311            throw new Exception(
 312              pht(
 313                'Package specification for "%s" includes resources of multiple '.
 314                'types (%s, %s). Each package may only contain one type of '.
 315                'resource.',
 316                $package_name,
 317                $type,
 318                $resource_type));
 319          }
 320  
 321          $hashes[] = $symbol.':'.$symbol_hash;
 322        }
 323  
 324        $hash = $resources->getCelerityHash(implode("\n", $hashes));
 325        $package_map[$package_name] = array(
 326          'hash' => $hash,
 327          'symbols' => $package_symbols,
 328        );
 329      }
 330  
 331      return $package_map;
 332    }
 333  
 334    private function mergeNameMaps(array $maps) {
 335      $result = array();
 336      $origin = array();
 337  
 338      foreach ($maps as $map) {
 339        list($map_name, $data) = $map;
 340        foreach ($data as $name => $hash) {
 341          if (empty($result[$name])) {
 342            $result[$name] = $hash;
 343            $origin[$name] = $map_name;
 344          } else {
 345            $old = $origin[$name];
 346            $new = $map_name;
 347            throw new Exception(
 348              pht(
 349                'Resource source defines two resources with the same name, '.
 350                '"%s". One is defined in the "%s" map; the other in the "%s" '.
 351                'map. Each resource must have a unique name.',
 352                $name,
 353                $old,
 354                $new));
 355          }
 356        }
 357      }
 358      return $result;
 359    }
 360  
 361  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1