Full Code of puli/repository for AI

master 2602e10e9718 cached
115 files
443.9 KB
109.1k tokens
894 symbols
1 requests
Download .txt
Showing preview only (477K chars total). Download the full file or copy to clipboard to get everything.
Repository: puli/repository
Branch: master
Commit: 2602e10e9718
Files: 115
Total size: 443.9 KB

Directory structure:
gitextract_vpugdgzo/

├── .composer-auth.json
├── .gitattributes
├── .gitignore
├── .styleci.yml
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── appveyor.yml
├── composer.json
├── phpunit.xml.dist
├── res/
│   └── schema/
│       └── path-mappings-schema-1.0.json
├── src/
│   ├── AbstractEditableRepository.php
│   ├── AbstractJsonRepository.php
│   ├── AbstractRepository.php
│   ├── Api/
│   │   ├── ChangeStream/
│   │   │   ├── ChangeStream.php
│   │   │   └── VersionList.php
│   │   ├── EditableRepository.php
│   │   ├── NoVersionFoundException.php
│   │   ├── Resource/
│   │   │   ├── BodyResource.php
│   │   │   ├── FilesystemResource.php
│   │   │   ├── PuliResource.php
│   │   │   └── ResourceMetadata.php
│   │   ├── ResourceCollection.php
│   │   ├── ResourceIterator.php
│   │   ├── ResourceNotFoundException.php
│   │   ├── ResourceRepository.php
│   │   ├── UnsupportedLanguageException.php
│   │   ├── UnsupportedOperationException.php
│   │   └── UnsupportedResourceException.php
│   ├── ChangeStream/
│   │   ├── InMemoryChangeStream.php
│   │   ├── JsonChangeStream.php
│   │   └── KeyValueStoreChangeStream.php
│   ├── Discovery/
│   │   ├── ResourceBinding.php
│   │   └── ResourceBindingInitializer.php
│   ├── FilesystemRepository.php
│   ├── InMemoryRepository.php
│   ├── JsonRepository.php
│   ├── NullRepository.php
│   ├── OptimizedJsonRepository.php
│   ├── RepositoryFactoryException.php
│   ├── Resource/
│   │   ├── AbstractFilesystemResource.php
│   │   ├── Collection/
│   │   │   ├── ArrayResourceCollection.php
│   │   │   ├── FilesystemResourceCollection.php
│   │   │   └── LazyResourceCollection.php
│   │   ├── DirectoryResource.php
│   │   ├── FileResource.php
│   │   ├── GenericResource.php
│   │   ├── Iterator/
│   │   │   ├── RecursiveResourceIterator.php
│   │   │   ├── RecursiveResourceIteratorIterator.php
│   │   │   ├── ResourceCollectionIterator.php
│   │   │   └── ResourceFilterIterator.php
│   │   ├── LinkResource.php
│   │   └── Metadata/
│   │       └── FilesystemMetadata.php
│   ├── StreamWrapper/
│   │   ├── ResourceStreamWrapper.php
│   │   ├── StreamWrapper.php
│   │   └── StreamWrapperException.php
│   └── Uri/
│       ├── InvalidUriException.php
│       └── Uri.php
└── tests/
    ├── AbstractEditableRepositoryTest.php
    ├── AbstractFilesystemRepositorySymlinkTest.php
    ├── AbstractFilesystemRepositoryTest.php
    ├── AbstractJsonRepositoryTest.php
    ├── AbstractRepositoryTest.php
    ├── Api/
    │   └── ChangeStream/
    │       └── VersionListTest.php
    ├── ChangeStream/
    │   ├── AbstractChangeStreamTest.php
    │   ├── InMemoryChangeStreamTest.php
    │   ├── JsonChangeStreamLoadedTest.php
    │   ├── JsonChangeStreamTest.php
    │   └── KeyValueStoreChangeStreamTest.php
    ├── Discovery/
    │   ├── Fixtures/
    │   │   └── SubResourceBinding.php
    │   ├── ResourceBindingInitializerTest.php
    │   └── ResourceBindingTest.php
    ├── FilesystemRepositoryAbsoluteSymlinkTest.php
    ├── FilesystemRepositoryCopyTest.php
    ├── FilesystemRepositoryLoadedTest.php
    ├── FilesystemRepositoryRelativeSymlinkTest.php
    ├── Fixtures/
    │   ├── dir1/
    │   │   ├── file1
    │   │   └── file2
    │   ├── dir2/
    │   │   ├── file2
    │   │   └── file3
    │   ├── dir3/
    │   │   └── sub/
    │   │       ├── file1
    │   │       └── file2
    │   ├── dir4/
    │   │   └── sub/
    │   │       ├── file2
    │   │       └── file3
    │   └── dir5/
    │       ├── file1
    │       ├── file2
    │       └── sub/
    │           ├── file3
    │           └── file4
    ├── InMemoryRepositoryTest.php
    ├── JsonRepositoryLoadedTest.php
    ├── JsonRepositoryTest.php
    ├── NullRepositoryTest.php
    ├── OptimizedJsonRepositoryLoadedTest.php
    ├── OptimizedJsonRepositoryTest.php
    ├── Resource/
    │   ├── AbstractFilesystemResourceTest.php
    │   ├── AbstractResourceTest.php
    │   ├── Collection/
    │   │   ├── ArrayResourceCollectionTest.php
    │   │   └── FilesystemResourceCollectionTest.php
    │   ├── DirectoryResourceTest.php
    │   ├── FileResourceTest.php
    │   ├── Fixtures/
    │   │   ├── dir1/
    │   │   │   ├── file1
    │   │   │   └── file2
    │   │   ├── dir2/
    │   │   │   ├── .dotfile
    │   │   │   └── file1
    │   │   └── file3
    │   ├── GenericResourceTest.php
    │   ├── Iterator/
    │   │   ├── ResourceCollectionIteratorTest.php
    │   │   └── ResourceFilterIteratorTest.php
    │   ├── LinkResourceTest.php
    │   ├── TestDirectory.php
    │   ├── TestFile.php
    │   └── TestMetadata.php
    ├── StreamWrapper/
    │   └── ResourceStreamWrapperTest.php
    └── Uri/
        └── UriTest.php

================================================
FILE CONTENTS
================================================

================================================
FILE: .composer-auth.json
================================================
{
    "github-oauth": {
        "github.com": "PLEASE DO NOT USE THIS TOKEN IN YOUR OWN PROJECTS/FORKS",
        "github.com": "This token is reserved for testing the puli/* repositories",
        "github.com": "a9debbffdd953ee9b3b82dbc3b807cde2086bb86"
    }
}


================================================
FILE: .gitattributes
================================================
# Path-based git attributes
# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html

# Ignore all test and documentation with "export-ignore".
/.composer-auth.json  export-ignore
/.gitattributes       export-ignore
/.gitignore           export-ignore
/.styleci.yml         export-ignore
/.travis.yml          export-ignore
/appveyor.yml         export-ignore
/phpunit.xml.dist     export-ignore
/tests                export-ignore


================================================
FILE: .gitignore
================================================
/vendor/
composer.lock


================================================
FILE: .styleci.yml
================================================
preset: symfony

enabled:
    - ordered_use
    - strict

disabled:
    - empty_return
    - phpdoc_annotation_without_dot # This is still buggy: https://github.com/symfony/symfony/pull/19198


================================================
FILE: .travis.yml
================================================
language: php

sudo: false

cache:
  directories:
    - $HOME/.composer/cache/files

matrix:
  include:
    - php: 5.3
    - php: 5.4
    - php: 5.5
    - php: 5.6
    - php: hhvm
    - php: nightly
    - php: 7.0
      env: COVERAGE=yes
    - php: 7.0
      env: COMPOSER_FLAGS='--prefer-lowest --prefer-stable'
  allow_failures:
    - php: hhvm
    - php: nightly
  fast_finish: true

before_install:
  - if [[ $TRAVIS_PHP_VERSION != hhvm && $COVERAGE != yes ]]; then phpenv config-rm xdebug.ini; fi;
  - if [[ $TRAVIS_REPO_SLUG = puli/repository ]]; then cp .composer-auth.json ~/.composer/auth.json; fi;
  - composer self-update

install: composer update $COMPOSER_FLAGS --prefer-dist --no-interaction

script: if [[ $COVERAGE = yes ]]; then vendor/bin/phpunit --verbose --coverage-clover=coverage.clover; else vendor/bin/phpunit --verbose; fi

after_script: if [[ $COVERAGE = yes ]]; then wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi

notifications:
  webhooks:
    urls: ['https://webhooks.gitter.im/e/9ccc2378e6c0de6480f8']
    on_success: change
    on_failure: always
    on_start:   never


================================================
FILE: CHANGELOG.md
================================================
Changelog
=========

* 1.0.0-beta11 (@release_date@)

 * added `ResourceBinding`
 * added `ResourceBindingInitializer`

* 1.0.0-beta10 (2016-02-05)

 * fixed regression in `FilesystemRepository::clear()` which caused files in
   symlinked directories to be deleted

* 1.0.0-beta9 (2016-01-14)

 * made compatible with Symfony 3.0
 * added JSON schema for path mapping files: `path-mappings-schema-1.0.json`
 * upgraded to webmozart/glob 4.1 to improve performance
 * renamed `Resource` to `PuliResource`
 * renamed `AbstractPathMappingRepository` to `AbstractJsonRepository`
 * renamed `PathMappingRepository` to `JsonRepository`
 * renamed `OptimizedPathMappingRepository` to `OptimizedJsonRepository`
 * changed constructor arguments of JSON repositories from `KeyValueStore`
   to paths of JSON files
 * added `AbstractEditableRepository`
 * added `ChangeStream`
 * added `VersionList`
 * added `NoVersionFoundException`
 * added `InMemoryChangeStream`
 * added `KeyValueStoreChangeStream`
 * added `JsonChangeStream`
 * added `PuliResource::getVersions()`
 * added `ResourceRepository::getVersions()`
 * added `LinkResource::getTarget()`
 * made `LinkResource` serializable

* 1.0.0-beta8 (2015-10-05)

 * fixed problem with slash handling in `PathMappingRepository`
 * added `LinkResource`

* 1.0.0-beta7 (2015-08-24)

 * improved Windows compatibility
 * fixed minimum package versions in composer.json
 * switched to webmozart/glob 3.1 to fix Windows issues
 * fixed resource overriding in the `PathMappingRepository`
 * supported removal of path mappings in `PathMappingRepository`

* 1.0.0-beta6 (2015-08-12)

 * added `PathMappingRepository`
 * added `OptimizedPathMappingRepository`
 * fixed repository building on Windows
 * upgraded to webmozart/glob 3.0 for enhanced performance of file iteration
 * added `AbstractRepository` and `AbstractPathMappingRepository`
 * fixed reading of file modification time for symlinks

* 1.0.0-beta5 (2015-05-29)

 * upgraded to webmozart/path-util 2.0
 * fixed overriding of files in deep directories

* 1.0.0-beta4 (2015-04-13)

 * removed `Resource::getPayload()`
 * removed `$code` arguments from static exception factory methods
 * upgraded to webmozart/glob 2.0

* 1.0.0-beta3 (2015-03-19)

 * added `Resource::getPayload()`
 * removed `DetachedException`
 * replaced `Assert` by webmozart/assert
 * added support for relative symlinks to `FilesystemRepository`
 * `FilesystemRepository` now falls back to copies if symlinks are not supported

* 1.0.0-beta2 (2015-01-27)

 * added `NullRepository`
 * removed dependency to beberlei/assert
 * symfony/filesystem is now an optional dependency that is only needed when
   using the FilesystemRepository

* 1.0.0-beta (2015-01-12)

 * renamed `Selector` to `Glob` and moved it to package "webmozart/glob"
 * removed `AttachableResourceInterface`
 * removed `DirectoryResourceInterface`
 * removed `FileResourceInterface`
 * removed `OverriddenPathLoaderInterface`
 * removed `Interface` suffix of all interfaces
 * `ResourceRepository::find()` now matches directory separators "/" when given
   a wildcard "*"
 * merged `AbstractResource` and `DirectoryResource` into `GenericResource`
 * renamed `LocalDirectoryResource` to `DirectoryResource`
 * renamed `LocalFileResource` to `FileResource`
 * removed `LocalResource::getAllLocalPaths`
 * rename `LocalResource::getLocalPath` to `LocalResource::getFilesystemPath`
 * renamed `LocalResource` to `FilesystemResource`
 * renamed `LocalResourceCollection` to `FilesystemResourceCollection`
 * removed `createAttached()` from `GenericResource`, `FileResource` and
   `DirectoryResource`
 * removed tagging
 * renamed`ResourceRepository` to `InMemoryRepository`
 * renamed `ResourceCollection` to `ArrayResourceCollection`
 * renamed `RecursiveResourceIterator` to `RecursiveResourceIteratorIterator`
 * renamed `ManageableResourceRepository` to `EditableRepository`
 * removed `UriRepository`
 * added `$scheme` argument to `ResourceStreamWrapper::register()` and
   `ResourceStreamWrapper::unregister()`
 * added `ResourceNotFoundException::forPath()`
 * added `NoDirectoryException::forPath()`
 * moved contents of `Puli\Repository\Filesystem\Iterator` to `Puli\Repository\Iterator`
 * moved contents of `Puli\Repository\Filesystem\Resource` to `Puli\Repository\Resource`
 * moved `FilesystemRepository` to `Puli\Repository`
 * removed `PhpCacheRepository`
 * added domain-specific `Assert` class
 * moved API interfaces to `Api` sub-namespace
 * removed notions of "directories" and "files". All resources can have children
   and a body now.
 * added `ResourceRepository::listChildren()` and `hasChildren()`
 * added `ResourceMetadata` and `FilesystemMetadata`
 * added methods to `Resource`:
   * `getChild()`
   * `hasChild()`
   * `hasChildren()`
   * `listChildren()`
   * `getMetadata()`
   * `getRepository()`
   * `getRepositoryPath()`
   * `attachTo()`
   * `detach()`
   * `isAttached()`
   * `createReference()`
   * `isReference()`
 * made `Resource` extend `Serializable`
 * added `EditableRepository::clear()`
 * removed backend repositories from `InMemoryRepository` and `FilesystemRepository`
 * added symlink support to `FilesystemRepository`
 * removed `FilesystemException`
 * removed `InvalidPathException`
 * removed `UnsupportedSchemeException`
 * replaced `NoDirectoryException` by `UnsupportedOperationException`
 * removed `CompositeRepository` from the 1.0 branch

* 1.0.0-alpha4 (2014-12-03)

 * moved extensions to separate repositories in https://github.com/puli
 * moved documentation to separate repository: https://github.com/puli/docs
 * moved `Path` to "webmozart/path-util" package
 * moved all code to `Puli\Repository` namespace
 * rearranged the directory structure
 * added `ResourceCollectionIterator`
 * added `ResourceIteratorInterface`
 * added `RecursiveResourceIterator`
 * added `RecursiveResourceIteratorInterface`
 * added `ResourceFilterIterator`
 * renamed `ResourceRepositoryInterface` to `ManageableRepositoryInterface`
 * renamed `ResourceLocatorInterface` to `ResourceRepositoryInterface`
 * renamed all "locators" to "repositories"
 * moved all filesystem specific code to `Filesystem` namespace
 * made `ResourceInterface` independent of the filesystem. The filesystem
   specific methods are now in `LocalResourceInterface`
 * `getAlternativePaths()` is now called `getAllLocalPaths()`
 * added `getContents()`, `getSize()`, `getLastAccessedAt()` and
   `getLastModifiedAt()` to `FileResourceInterface`
 * removed all pattern-related classes. This logic is now provided by the
   `Selector` class
 * `ResourceRepository::remove()`, `tag()` and `untag()` now return the number
   of affected resources
 * added `UriRepository::getDefaultScheme()` and `setDefaultScheme()`
 * renamed `getByTag()` to `findByTag()`
 * added `merge()` to `ResourceCollectionInterface`
 * added `CompositeRepository`
 * removed `LazyDirectoryResource`
 * fixed ResourceRepository::add() to be deterministic when selectors are passed. Closes #17

* 1.0.0-alpha3 (2014-02-22)

 * renamed `PhpResourceLocator` to `PhpCacheLocator`
 * renamed `PhpResourceLocatorDumper` to `PhpCacheDumper`
 * added `FilesystemLocator`
 * removed `ResourceDiscoveringInterface`
 * a base `ResourceLocatorInterface` can now be passed to `ResourceRepository`
 * instead of arrays, `ResourceCollection` objects are now returned everywhere
 * renamed `ResourceInterface::getPath()` to `getRealPath()`
 * renamed `ResourceInterface::getRepositoryPath()` to `getPath()`
 * added an extension for the templating engine Twig
 * added an extension for the Symfony Config and HttpKernel components

* 1.0.0-alpha2 (2014-02-14)

 * fixed "Maximum function nesting level" error on Windows
 * pushed minimum PHP version to 5.3.9
 * removed `TagInterface` and descending classes
 * added support for dot segments ("." and "..")
 * removed `CreationNotAllowedException`
 * removed `RemoveNotAllowedException`
 * removed `RenameNotAllowedException`
 * added `UnsupportedOperationException`
 * added `Path`
 * added `Uri`
 * added `UriLocatorInterface` and `UriLocator`
 * changed `ResourceStreamWrapper::register()` to take a `UriLocatorInterface`
   instance instead of a scheme and a resource locator

* 1.0.0-alpha1 (2014-02-04)

 * first alpha release


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2014 Bernhard Schussek

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
The Puli Repository Component
=============================

[![Build Status](https://travis-ci.org/puli/repository.svg?branch=1.0)](https://travis-ci.org/puli/repository)
[![Build status](https://ci.appveyor.com/api/projects/status/a0g5jdtj78wv53c0/branch/1.0?svg=true)](https://ci.appveyor.com/project/webmozart/repository/branch/1.0)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/puli/repository/badges/quality-score.png?b=1.0)](https://scrutinizer-ci.com/g/puli/repository/?branch=1.0)
[![Latest Stable Version](https://poser.pugx.org/puli/repository/v/stable.svg)](https://packagist.org/packages/puli/repository)
[![Total Downloads](https://poser.pugx.org/puli/repository/downloads.svg)](https://packagist.org/packages/puli/repository)
[![Dependency Status](https://www.versioneye.com/php/puli:repository/1.0.0/badge.svg)](https://www.versioneye.com/php/puli:repository/1.0.0)

Latest release: [1.0.0-beta10](https://packagist.org/packages/puli/repository#1.0.0-beta10)

PHP >= 5.3.9

The [Puli] Repository Component provides an API for storing arbitrary resources
in a filesystem-like repository:

```php
use Puli\Repository\InMemoryRepository;
use Puli\Repository\Resource\DirectoryResource;

$repo = new InMemoryRepository();
$repo->add('/config', new DirectoryResource('/path/to/resources/config'));

// /path/to/resources/config/routing.yml
echo $repo->get('/config/routing.yml')->getBody();
```

The following [`ResourceRepository`] implementations are currently supported:

* [`InMemoryRepository`]
* [`FilesystemRepository`]
* [`NullRepository`]
* [`JsonRepository`]
* [`OptimizedJsonRepository`]

The following [`Resource`] implementations are currently supported:

* [`GenericResource`]
* [`FileResource`]
* [`DirectoryResource`]
* [`LinkResource`]

Authors
-------

* [Bernhard Schussek] a.k.a. [@webmozart]
* [The Community Contributors]

Installation
------------

Follow the [Getting Started] guide to install Puli in your project.

Documentation
-------------

Read the [Puli Documentation] to learn more about Puli.

Contribute
----------

Contributions to Puli are always welcome!

* Report any bugs or issues you find on the [issue tracker].
* You can grab the source code at Puli’s [Git repository].

Support
-------

If you are having problems, send a mail to bschussek@gmail.com or shout out to
[@webmozart] on Twitter.

License
-------

All contents of this package are licensed under the [MIT license].

[Puli]: http://puli.io
[Bernhard Schussek]: http://webmozarts.com
[The Community Contributors]: https://github.com/puli/repository/graphs/contributors
[Installation guide]: http://docs.puli.io/en/latest/installation.html
[Puli Documentation]: http://docs.puli.io/en/latest/index.html
[issue tracker]: https://github.com/puli/issues/issues
[Git repository]: https://github.com/puli/repository
[@webmozart]: https://twitter.com/webmozart
[MIT license]: LICENSE
[`ResourceRepository`]: http://api.puli.io/latest/class-Puli.Repository.Api.ResourceRepository.html
[`InMemoryRepository`]: http://api.puli.io/latest/class-Puli.Repository.InMemoryRepository.html
[`FilesystemRepository`]: http://api.puli.io/latest/class-Puli.Repository.FilesystemRepository.html
[`NullRepository`]: http://api.puli.io/latest/class-Puli.Repository.NullRepository.html
[`JsonRepository`]: http://api.puli.io/latest/class-Puli.Repository.JsonRepository.html
[`OptimizedJsonRepository`]: http://api.puli.io/latest/class-Puli.Repository.OptimizedJsonRepository.html
[`Resource`]: http://api.puli.io/latest/class-Puli.Repository.Api.Resource.Resource.html
[`GenericResource`]: http://api.puli.io/latest/class-Puli.Repository.Resource.GenericResource.html
[`FileResource`]: http://api.puli.io/latest/class-Puli.Repository.Resource.FileResource.html
[`DirectoryResource`]: http://api.puli.io/latest/class-Puli.Repository.Resource.DirectoryResource.html
[`LinkResource`]: http://api.puli.io/latest/class-Puli.Repository.Resource.LinkResource.html


================================================
FILE: appveyor.yml
================================================
build: false
platform: x86
clone_folder: c:\projects\puli\repository

cache:
  - c:\php -> appveyor.yml

init:
  - SET PATH=c:\php;%PATH%
  - SET COMPOSER_NO_INTERACTION=1
  - SET PHP=1

install:
  - IF EXIST c:\php (SET PHP=0) ELSE (mkdir c:\php)
  - cd c:\php
  - IF %PHP%==1 appveyor DownloadFile http://windows.php.net/downloads/releases/archives/php-7.0.0-nts-Win32-VC14-x86.zip
  - IF %PHP%==1 7z x php-7.0.0-nts-Win32-VC14-x86.zip -y >nul
  - IF %PHP%==1 del /Q *.zip
  - IF %PHP%==1 echo @php %%~dp0composer.phar %%* > composer.bat
  - IF %PHP%==1 copy /Y php.ini-development php.ini
  - IF %PHP%==1 echo max_execution_time=1200 >> php.ini
  - IF %PHP%==1 echo date.timezone="UTC" >> php.ini
  - IF %PHP%==1 echo extension_dir=ext >> php.ini
  - IF %PHP%==1 echo extension=php_openssl.dll >> php.ini
  - IF %PHP%==1 echo extension=php_mbstring.dll >> php.ini
  - IF %PHP%==1 echo extension=php_fileinfo.dll >> php.ini
  - appveyor DownloadFile https://getcomposer.org/composer.phar
  - cd c:\projects\puli\repository
  - mkdir %APPDATA%\Composer
  - IF %APPVEYOR_REPO_NAME%==puli/repository copy /Y .composer-auth.json %APPDATA%\Composer\auth.json
  - composer update --prefer-dist --no-progress --ansi

test_script:
  - cd c:\projects\puli\repository
  - vendor\bin\phpunit.bat --verbose


================================================
FILE: composer.json
================================================
{
    "name": "puli/repository",
    "description": "A filesystem-like repository for storing arbitrary resources.",
    "homepage": "http://puli.io",
    "license": "MIT",
    "authors": [
        {
            "name": "Bernhard Schussek",
            "email": "bschussek@gmail.com"
        }
    ],
    "require": {
        "php": "^5.3.9|^7.0",
        "webmozart/path-util": "^2.2",
        "webmozart/glob": "^4.1",
        "webmozart/assert": "^1.0",
        "psr/log": "^1.0"
    },
    "require-dev": {
        "puli/discovery": "^1.0-beta10@dev",
        "webmozart/json": "^1.2.1",
        "webmozart/key-value-store": "^1.0-beta7",
        "symfony/filesystem": "^2.0|^3.0",
        "phpunit/phpunit": "^4.6",
        "sebastian/version": "^1.0.1"
    },
    "suggest": {
        "puli/discovery": "to provide and discover resources across modules",
        "webmozart/json": "to use the JSON repositories",
        "webmozart/key-value-store": "to use the key-value store change stream",
        "symfony/filesystem": "to use the filesystem repository"
    },
    "autoload": {
        "psr-4": {
            "Puli\\Repository\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Puli\\Repository\\Tests\\": "tests/"
        }
    },
    "extra": {
        "branch-alias": {
            "dev-master": "1.0-dev"
        }
    },
    "support": {
        "issues": "https://github.com/puli/issues/issues"
    }
}


================================================
FILE: phpunit.xml.dist
================================================
<?xml version="1.0" encoding="UTF-8"?>

<phpunit bootstrap="vendor/autoload.php" colors="true">
    <testsuites>
        <testsuite name="Puli Test Suite">
            <directory suffix="Test.php">./tests/</directory>
        </testsuite>
    </testsuites>

    <!-- Whitelist for code coverage -->
    <filter>
        <whitelist>
            <directory suffix=".php">./src/</directory>
        </whitelist>
    </filter>
</phpunit>


================================================
FILE: res/schema/path-mappings-schema-1.0.json
================================================
{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "type": "object",
    "description": "A list of mappings from Puli paths to filesystem paths or other Puli paths.",
    "definitions": {
        "relativePath": {
            "description": "A filesystem path relative to the base directory of the repository.",
            "type": "string",
            "pattern": "^([a-zA-Z][^:]|[^/@a-zA-Z])[^/]*(/[^/]+)*$"
        },
        "absolutePath": {
            "description": "An absolute filesystem path.",
            "type": "string",
            "pattern": "^([a-zA-Z]:)?/(([^/]+/)*[^/]+)?$"
        },
        "virtualDirectory": {
            "description": "A directory in the Puli repository that does not correspond to a real filesystem directory.",
            "type": "null"
        },
        "repositoryLink": {
            "description": "A link to another resource in the Puli repository.",
            "type": "string",
            "pattern": "^@/(([^/]+/)*[^/]+)?$"
        },
        "singleReference": {
            "description": "A reference to a single file, directory, link or virtual directory.",
            "oneOf": [
                { "$ref": "#/definitions/relativePath" },
                { "$ref": "#/definitions/absolutePath" },
                { "$ref": "#/definitions/repositoryLink" },
                { "$ref": "#/definitions/virtualDirectory" }
            ]
        },
        "combinedReference": {
            "description": "A list of references. The first reference has highest priority. Directory contents are merged.",
            "type": "array",
            "minItems": 2,
            "uniqueItems": true,
            "items": { "$ref": "#/definitions/singleReference" }
        }
    },
    "patternProperties": {
        "^/(([^/]+/)*[^/]+)?$": {
            "description": "A mapping from a Puli path to one or more path references.",
            "oneOf": [
                { "$ref": "#/definitions/singleReference" },
                { "$ref": "#/definitions/combinedReference" }
            ]
        }
    },
    "properties": {
        "_order": {
            "description": "Overrides the default resolution order. By default, mappings for longer paths (e.g. /a/b) have precedence over mapping for shorter paths (e.g. /a). This can be overridden here.",
            "type": "object",
            "patternProperties": {
                "^/(([^/]+/)*[^/]+)?$": {
                    "description": "Overrides the resolution order for a mapped Puli path. The repository will check the path mappings referred to by the entries of the array in the given order.",
                    "type": "array",
                    "minItems": 2,
                    "items": {
                        "description": "Contains the path of a path mapping and the number of references to check of that path mapping.",
                        "type": "object",
                        "properties": {
                            "path": {
                                "description": "The Puli path of a mapping in this file.",
                                "type": "string",
                                "pattern": "^/(([^/]+/)*[^/]+)?$"
                            },
                            "references": {
                                "description": "The number of references of the mapping to check.",
                                "type": "integer",
                                "min": 1
                            }
                        }
                    }
                }
            }
        }
    },
    "additionalProperties": false
}


================================================
FILE: src/AbstractEditableRepository.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository;

use Puli\Repository\Api\ChangeStream\ChangeStream;
use Puli\Repository\Api\EditableRepository;
use Puli\Repository\Api\Resource\PuliResource;

/**
 * Abstract base for editable repositories providing tools to avoid code duplication.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 * @author Titouan Galopin <galopintitouan@gmail.com>
 */
abstract class AbstractEditableRepository extends AbstractRepository implements EditableRepository
{
    /**
     * @var ChangeStream
     */
    private $changeStream;

    /**
     * Create the repository.
     *
     * @param ChangeStream|null $changeStream If provided, the repository will log
     *                                        resources changes in this change stream.
     */
    public function __construct(ChangeStream $changeStream = null)
    {
        $this->changeStream = $changeStream;
    }

    /**
     * {@inheritdoc}
     */
    public function getVersions($path)
    {
        if (null === $this->changeStream) {
            return parent::getVersions($path);
        }

        return $this->changeStream->getVersions($path, $this);
    }

    /**
     * Stores a version of a resource in the change stream.
     *
     * @param PuliResource $resource The resource version.
     */
    protected function storeVersion(PuliResource $resource)
    {
        if (null !== $this->changeStream) {
            $this->changeStream->append($resource);
        }
    }

    /**
     * Removes all versions of a resource from the change stream.
     *
     * @param string $path The Puli path.
     */
    protected function removeVersions($path)
    {
        if (null !== $this->changeStream) {
            $this->changeStream->purge($path);
        }
    }

    /**
     * Clears the change stream.
     */
    protected function clearVersions()
    {
        if (null !== $this->changeStream) {
            $this->changeStream->clear();
        }
    }
}


================================================
FILE: src/AbstractJsonRepository.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository;

use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Puli\Repository\Api\ChangeStream\ChangeStream;
use Puli\Repository\Api\Resource\FilesystemResource;
use Puli\Repository\Api\Resource\PuliResource;
use Puli\Repository\Api\ResourceCollection;
use Puli\Repository\Api\ResourceNotFoundException;
use Puli\Repository\Api\UnsupportedResourceException;
use Puli\Repository\Resource\Collection\ArrayResourceCollection;
use Puli\Repository\Resource\DirectoryResource;
use Puli\Repository\Resource\FileResource;
use Puli\Repository\Resource\GenericResource;
use Puli\Repository\Resource\LinkResource;
use RuntimeException;
use Webmozart\Assert\Assert;
use Webmozart\Json\JsonDecoder;
use Webmozart\Json\JsonEncoder;
use Webmozart\PathUtil\Path;

/**
 * Base class for repositories backed by a JSON file.
 *
 * The generated JSON file is described by res/schema/repository-schema-1.0.json.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 * @author Titouan Galopin <galopintitouan@gmail.com>
 */
abstract class AbstractJsonRepository extends AbstractEditableRepository implements LoggerAwareInterface
{
    /**
     * Flag: Whether to stop after the first result.
     *
     * @internal
     */
    const STOP_ON_FIRST = 1;

    /**
     * @var array
     */
    protected $json;

    /**
     * @var string
     */
    protected $baseDirectory;

    /**
     * @var string
     */
    private $path;

    /**
     * @var string
     */
    private $schemaPath;

    /**
     * @var JsonEncoder
     */
    private $encoder;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * Creates a new repository.
     *
     * @param string            $path          The path to the JSON file. If
     *                                         relative, it must be relative to
     *                                         the base directory.
     * @param string            $baseDirectory The base directory of the store.
     *                                         Paths inside that directory are
     *                                         stored as relative paths. Paths
     *                                         outside that directory are stored
     *                                         as absolute paths.
     * @param bool              $validateJson  Whether to validate the JSON file
     *                                         against the schema. Slow but
     *                                         spots problems.
     * @param ChangeStream|null $changeStream  If provided, the repository will
     *                                         append resource changes to this
     *                                         change stream.
     */
    public function __construct($path, $baseDirectory, $validateJson = false, ChangeStream $changeStream = null)
    {
        parent::__construct($changeStream);

        $this->baseDirectory = $baseDirectory;
        $this->path = Path::makeAbsolute($path, $baseDirectory);
        $this->encoder = new JsonEncoder();
        $this->encoder->setPrettyPrinting(true);
        $this->encoder->setEscapeSlash(false);

        if ($validateJson) {
            $this->schemaPath = Path::canonicalize(__DIR__.'/../res/schema/path-mappings-schema-1.0.json');
        }
    }

    /**
     * {@inheritdoc}
     */
    public function setLogger(LoggerInterface $logger = null)
    {
        $this->logger = $logger;
    }

    /**
     * {@inheritdoc}
     */
    public function add($path, $resource)
    {
        if (null === $this->json) {
            $this->load();
        }

        $path = $this->sanitizePath($path);

        if ($resource instanceof ResourceCollection) {
            $this->ensureDirectoryExists($path);

            foreach ($resource as $child) {
                $this->addResource($path.'/'.$child->getName(), $child);
            }

            $this->flush();

            return;
        }

        $this->ensureDirectoryExists(Path::getDirectory($path));

        $this->addResource($path, $resource);

        $this->flush();
    }

    /**
     * {@inheritdoc}
     */
    public function get($path)
    {
        if (null === $this->json) {
            $this->load();
        }

        $path = $this->sanitizePath($path);
        $references = $this->getReferencesForPath($path);

        // Might be null, don't use isset()
        if (array_key_exists($path, $references)) {
            return $this->createResource($path, $references[$path]);
        }

        throw ResourceNotFoundException::forPath($path);
    }

    /**
     * {@inheritdoc}
     */
    public function find($query, $language = 'glob')
    {
        if (null === $this->json) {
            $this->load();
        }

        $this->failUnlessGlob($language);
        $query = $this->sanitizePath($query);
        $results = $this->createResources($this->getReferencesForGlob($query));

        ksort($results);

        return new ArrayResourceCollection(array_values($results));
    }

    /**
     * {@inheritdoc}
     */
    public function contains($query, $language = 'glob')
    {
        if (null === $this->json) {
            $this->load();
        }

        $this->failUnlessGlob($language);
        $query = $this->sanitizePath($query);

        $results = $this->getReferencesForGlob($query, self::STOP_ON_FIRST);

        return !empty($results);
    }

    /**
     * {@inheritdoc}
     */
    public function remove($query, $language = 'glob')
    {
        if (null === $this->json) {
            $this->load();
        }

        $this->failUnlessGlob($language);
        $query = $this->sanitizePath($query);

        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');

        $removed = $this->removeReferences($query);

        $this->flush();

        return $removed;
    }

    /**
     * {@inheritdoc}
     */
    public function clear()
    {
        if (null === $this->json) {
            $this->load();
        }

        // Subtract root which is not deleted
        $removed = count($this->getReferencesForRegex('/', '~.~')) - 1;

        $this->json = array();

        $this->flush();

        return $removed;
    }

    /**
     * {@inheritdoc}
     */
    public function listChildren($path)
    {
        if (null === $this->json) {
            $this->load();
        }

        $path = $this->sanitizePath($path);
        $results = $this->createResources($this->getReferencesInDirectory($path));

        if (empty($results)) {
            $pathResults = $this->getReferencesForPath($path);

            if (empty($pathResults)) {
                throw ResourceNotFoundException::forPath($path);
            }
        }

        ksort($results);

        return new ArrayResourceCollection(array_values($results));
    }

    /**
     * {@inheritdoc}
     */
    public function hasChildren($path)
    {
        if (null === $this->json) {
            $this->load();
        }

        $path = $this->sanitizePath($path);

        $results = $this->getReferencesInDirectory($path, self::STOP_ON_FIRST);

        if (empty($results)) {
            $pathResults = $this->getReferencesForPath($path);

            if (empty($pathResults)) {
                throw ResourceNotFoundException::forPath($path);
            }

            return false;
        }

        return true;
    }

    /**
     * Inserts a path reference into the JSON file.
     *
     * The path reference can be:
     *
     *  * a link starting with `@`
     *  * an absolute filesystem path
     *
     * @param string      $path      The Puli path.
     * @param string|null $reference The path reference.
     */
    abstract protected function insertReference($path, $reference);

    /**
     * Removes all path references matching the given glob from the JSON file.
     *
     * @param string $glob The glob for a list of Puli paths.
     */
    abstract protected function removeReferences($glob);

    /**
     * Returns the references for a given Puli path.
     *
     * Each reference returned by this method can be:
     *
     *  * `null`
     *  * a link starting with `@`
     *  * an absolute filesystem path
     *
     * The result has either one entry or none, if no path was found. The key
     * of the single entry is the path passed to this method.
     *
     * @param string $path The Puli path.
     *
     * @return string[]|null[] A one-level array of references with Puli paths
     *                         as keys. The array has at most one entry.
     */
    abstract protected function getReferencesForPath($path);

    /**
     * Returns the references matching a given Puli path glob.
     *
     * Each reference returned by this method can be:
     *
     *  * `null`
     *  * a link starting with `@`
     *  * an absolute filesystem path
     *
     * The keys of the returned array are Puli paths. Their order is undefined.
     *
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
     *
     * @param string $glob  The glob.
     * @param int    $flags A bitwise combination of the flag constants in this
     *                      class.
     *
     * @return string[]|null[] A one-level array of references with Puli paths
     *                         as keys.
     */
    abstract protected function getReferencesForGlob($glob, $flags = 0);

    /**
     * Returns the references matching a given Puli path regular expression.
     *
     * Each reference returned by this method can be:
     *
     *  * `null`
     *  * a link starting with `@`
     *  * an absolute filesystem path
     *
     * The keys of the returned array are Puli paths. Their order is undefined.
     *
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
     *
     * @param string $staticPrefix The static prefix of all Puli paths matching
     *                             the regular expression.
     * @param string $regex        The regular expression.
     * @param int    $flags        A bitwise combination of the flag constants
     *                             in this class.
     *
     * @return string[]|null[] A one-level array of references with Puli paths
     *                         as keys.
     */
    abstract protected function getReferencesForRegex($staticPrefix, $regex, $flags = 0);

    /**
     * Returns the references in a given Puli path.
     *
     * Each reference returned by this method can be:
     *
     *  * `null`
     *  * a link starting with `@`
     *  * an absolute filesystem path
     *
     * The keys of the returned array are Puli paths. Their order is undefined.
     *
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
     *
     * @param string $path  The Puli path.
     * @param int    $flags A bitwise combination of the flag constants in this
     *                      class.
     *
     * @return string[]|null[] A one-level array of references with Puli paths
     *                         as keys.
     */
    abstract protected function getReferencesInDirectory($path, $flags = 0);

    /**
     * Logs a message.
     *
     * @param mixed  $level   One of the level constants in {@link LogLevel}.
     * @param string $message The message.
     */
    protected function log($level, $message)
    {
        if (null !== $this->logger) {
            $this->logger->log($level, $message);
        }
    }

    /**
     * Logs a warning that a reference could not be found.
     *
     * @param string $path              The Puli path of a path mapping.
     * @param string $reference         The reference that was not found.
     * @param string $absoluteReference The absolute filesystem path of the
     *                                  reference.
     */
    protected function logReferenceNotFound($path, $reference, $absoluteReference)
    {
        $this->log(LogLevel::WARNING, sprintf(
            'The reference "%s"%s mapped by the path %s could not be found.',
            $reference,
            $reference !== $absoluteReference ? ' ('.$absoluteReference.')' : '',
            $path
        ));
    }

    /**
     * Adds a filesystem resource to the JSON file.
     *
     * @param string             $path     The Puli path.
     * @param FilesystemResource $resource The resource to add.
     */
    protected function addFilesystemResource($path, FilesystemResource $resource)
    {
        $resource = clone $resource;
        $resource->attachTo($this, $path);

        $relativePath = Path::makeRelative($resource->getFilesystemPath(), $this->baseDirectory);

        $this->insertReference($path, $relativePath);

        $this->storeVersion($resource);
    }

    /**
     * Loads the JSON file.
     */
    protected function load()
    {
        $decoder = new JsonDecoder();

        $this->json = file_exists($this->path)
            ? (array) $decoder->decodeFile($this->path, $this->schemaPath)
            : array();

        if (isset($this->json['_order'])) {
            $this->json['_order'] = (array) $this->json['_order'];

            foreach ($this->json['_order'] as $path => $entries) {
                foreach ($entries as $key => $entry) {
                    $this->json['_order'][$path][$key] = (array) $entry;
                }
            }
        }

        // The root node always exists
        if (!isset($this->json['/'])) {
            $this->json['/'] = null;
        }

        // Make sure the JSON is sorted in reverse order
        krsort($this->json);
    }

    /**
     * Writes the JSON file.
     */
    protected function flush()
    {
        // The root node always exists
        if (!isset($this->json['/'])) {
            $this->json['/'] = null;
        }

        // Always save in reverse order
        krsort($this->json);

        // Comply to schema
        $json = (object) $this->json;

        if (isset($json->{'_order'})) {
            $order = $json->{'_order'};

            foreach ($order as $path => $entries) {
                foreach ($entries as $key => $entry) {
                    $order[$path][$key] = (object) $entry;
                }
            }

            $json->{'_order'} = (object) $order;
        }

        $this->encoder->encodeFile($json, $this->path, $this->schemaPath);
    }

    /**
     * Returns whether a reference contains a link.
     *
     * @param string $reference The reference.
     *
     * @return bool Whether the reference contains a link.
     */
    protected function isLinkReference($reference)
    {
        return isset($reference[0]) && '@' === $reference[0];
    }

    /**
     * Returns whether a reference contains an absolute or relative filesystem
     * path.
     *
     * @param string $reference The reference.
     *
     * @return bool Whether the reference contains a filesystem path.
     */
    protected function isFilesystemReference($reference)
    {
        return null !== $reference && !$this->isLinkReference($reference);
    }

    /**
     * Turns a reference into a resource.
     *
     * @param string      $path      The Puli path.
     * @param string|null $reference The reference.
     *
     * @return PuliResource The resource.
     */
    protected function createResource($path, $reference)
    {
        if (null === $reference) {
            $resource = new GenericResource();
        } elseif (isset($reference[0]) && '@' === $reference[0]) {
            $resource = new LinkResource(substr($reference, 1));
        } elseif (is_dir($reference)) {
            $resource = new DirectoryResource($reference);
        } elseif (is_file($reference)) {
            $resource = new FileResource($reference);
        } else {
            throw new RuntimeException(sprintf(
                'Trying to create a FilesystemResource on a non-existing file or directory "%s"',
                $reference
            ));
        }

        $resource->attachTo($this, $path);

        return $resource;
    }

    /**
     * Turns a list of references into a list of resources.
     *
     * The references are expected to be in the format returned by
     * {@link getReferencesForPath()}, {@link getReferencesForGlob()} and
     * {@link getReferencesInDirectory()}.
     *
     * The result contains Puli paths as keys and {@link PuliResource}
     * implementations as values. The order of the results is undefined.
     *
     * @param string[]|null[] $references The references indexed by Puli paths.
     *
     * @return array
     */
    private function createResources(array $references)
    {
        foreach ($references as $path => $reference) {
            $references[$path] = $this->createResource($path, $reference);
        }

        return $references;
    }

    /**
     * Adds all ancestor directories of a path to the repository.
     *
     * @param string $path A Puli path.
     */
    private function ensureDirectoryExists($path)
    {
        if (array_key_exists($path, $this->json)) {
            return;
        }

        // Recursively initialize parent directories
        if ('/' !== $path) {
            $this->ensureDirectoryExists(Path::getDirectory($path));
        }

        $this->json[$path] = null;
    }

    /**
     * Adds a resource to the repository.
     *
     * @param string                          $path     The Puli path to add the
     *                                                  resource at.
     * @param FilesystemResource|LinkResource $resource The resource to add.
     */
    private function addResource($path, $resource)
    {
        if (!$resource instanceof FilesystemResource && !$resource instanceof LinkResource) {
            throw new UnsupportedResourceException(sprintf(
                'The %s only supports adding FilesystemResource and '.
                'LinkedResource instances. Got: %s',
                // Get the short class name
                $this->getShortClassName(get_class($this)),
                $this->getShortClassName(get_class($resource))
            ));
        }

        if ($resource instanceof LinkResource) {
            $resource = clone $resource;
            $resource->attachTo($this, $path);

            $this->insertReference($path, '@'.$resource->getTargetPath());

            $this->storeVersion($resource);
        } else {
            // Extension point for the optimized repository
            $this->addFilesystemResource($path, $resource);
        }
    }

    /**
     * Returns the short name of a fully-qualified class name.
     *
     * @param string $className The fully-qualified class name.
     *
     * @return string The short class name.
     */
    private function getShortClassName($className)
    {
        if (false !== ($pos = strrpos($className, '\\'))) {
            return substr($className, $pos + 1);
        }

        return $className;
    }
}


================================================
FILE: src/AbstractRepository.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository;

use Puli\Repository\Api\ChangeStream\VersionList;
use Puli\Repository\Api\NoVersionFoundException;
use Puli\Repository\Api\ResourceNotFoundException;
use Puli\Repository\Api\ResourceRepository;
use Puli\Repository\Api\UnsupportedLanguageException;
use Webmozart\Assert\Assert;
use Webmozart\PathUtil\Path;

/**
 * Abstract base for repositories providing tools to avoid code duplication.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 * @author Titouan Galopin <galopintitouan@gmail.com>
 */
abstract class AbstractRepository implements ResourceRepository
{
    /**
     * {@inheritdoc}
     */
    public function getVersions($path)
    {
        // Non-editable repositories always contain only one version of a resource
        try {
            return new VersionList($path, array($this->get($path)));
        } catch (ResourceNotFoundException $e) {
            throw NoVersionFoundException::forPath($path, $e);
        }
    }

    /**
     * Validate a language is usable to search in repositories.
     *
     * @param string $language
     */
    protected function failUnlessGlob($language)
    {
        if ('glob' !== $language) {
            throw UnsupportedLanguageException::forLanguage($language);
        }
    }

    /**
     * Sanitize a given path and check its validity.
     *
     * @param string $path
     *
     * @return string
     */
    protected function sanitizePath($path)
    {
        Assert::stringNotEmpty($path, 'The path must be a non-empty string. Got: %s');
        Assert::startsWith($path, '/', 'The path %s is not absolute.');

        return Path::canonicalize($path);
    }
}


================================================
FILE: src/Api/ChangeStream/ChangeStream.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Api\ChangeStream;

use Puli\Repository\Api\NoVersionFoundException;
use Puli\Repository\Api\Resource\PuliResource;
use Puli\Repository\Api\ResourceRepository;

/**
 * Tracks different versions of a resource.
 *
 * @since  1.0
 *
 * @author Titouan Galopin <galopintitouan@gmail.com>
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
interface ChangeStream
{
    /**
     * Stores a new version of a resource.
     *
     * @param PuliResource $resource The resource to store.
     */
    public function append(PuliResource $resource);

    /**
     * Removes all versions stored for a path.
     *
     * @param string $path The Puli path.
     */
    public function purge($path);

    /**
     * Returns whether the stream contains any version for a path.
     *
     * @param string $path The Puli path.
     *
     * @return bool Returns `true` if a version can be found and `false` otherwise.
     */
    public function contains($path);

    /**
     * Removes all contents of the stream.
     */
    public function clear();

    /**
     * Returns all versions of a resource.
     *
     * @param string             $path       The Puli path to look for.
     * @param ResourceRepository $repository The repository to attach the
     *                                       resources to.
     *
     * @return VersionList The versions of the resource.
     *
     * @throws NoVersionFoundException If no version is found for the path.
     */
    public function getVersions($path, ResourceRepository $repository = null);
}


================================================
FILE: src/Api/ChangeStream/VersionList.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Api\ChangeStream;

use ArrayAccess;
use ArrayIterator;
use BadMethodCallException;
use Countable;
use IteratorAggregate;
use OutOfBoundsException;
use Puli\Repository\Api\Resource\PuliResource;
use Webmozart\Assert\Assert;

/**
 * Contains different versions of a resource.
 *
 * @since  1.0
 *
 * @author Titouan Galopin <galopintitouan@gmail.com>
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class VersionList implements IteratorAggregate, ArrayAccess, Countable
{
    /**
     * @var string
     */
    private $path;

    /**
     * @var array
     */
    private $versions;

    /**
     * Creates a new version list.
     *
     * @param string         $path     The Puli path.
     * @param PuliResource[] $versions The versions of the resource, starting
     *                                 with the first.
     */
    public function __construct($path, array $versions)
    {
        Assert::stringNotEmpty($path, 'The Puli path must be a non-empty string. Got: %s');
        Assert::allIsInstanceOf($versions, 'Puli\Repository\Api\Resource\PuliResource');
        Assert::greaterThanEq(count($versions), 1, 'Expected at least one version.');

        $this->path = $path;
        $this->versions = array_values($versions);
    }

    /**
     * Returns the path of the versioned resources.
     *
     * @return string The Puli path.
     */
    public function getPath()
    {
        return $this->path;
    }

    /**
     * Returns the current version of the resource.
     *
     * @return PuliResource The current version.
     */
    public function getCurrent()
    {
        return $this->get($this->getCurrentVersion());
    }

    /**
     * Returns the current version number.
     *
     * @return int The current version number.
     */
    public function getCurrentVersion()
    {
        return count($this->versions) - 1;
    }

    /**
     * Returns the first version of the resource.
     *
     * @return PuliResource The first version.
     */
    public function getFirst()
    {
        return $this->get($this->getFirstVersion());
    }

    /**
     * Returns the first version number.
     *
     * @return int The first version number.
     */
    public function getFirstVersion()
    {
        return 0;
    }

    /**
     * Returns whether a specific version exists.
     *
     * @param int $version The version number starting at 0.
     *
     * @return bool Whether the version exists.
     */
    public function contains($version)
    {
        return isset($this->versions[$version]);
    }

    /**
     * Returns a specific version of the resource.
     *
     * @param int $version The version number starting at 0.
     *
     * @return PuliResource The resource.
     *
     * @throws OutOfBoundsException If the version number does not exist.
     */
    public function get($version)
    {
        if (!isset($this->versions[$version])) {
            throw new OutOfBoundsException(sprintf(
                'The version %s of path %s does not exist.',
                $version,
                $this->path
            ));
        }

        return $this->versions[$version];
    }

    /**
     * Returns all version numbers.
     *
     * @return int[] The version numbers.
     */
    public function getVersions()
    {
        return array_keys($this->versions);
    }

    /**
     * Returns the list as array indexed by version numbers.
     *
     * @return PuliResource[] The resource versions indexed by their version numbers.
     */
    public function toArray()
    {
        return $this->versions;
    }

    /**
     * {@inheritdoc}
     */
    public function getIterator()
    {
        return new ArrayIterator($this->versions);
    }

    /**
     * {@inheritdoc}
     */
    public function offsetExists($offset)
    {
        return $this->contains($offset);
    }

    /**
     * {@inheritdoc}
     */
    public function offsetGet($offset)
    {
        return $this->get($offset);
    }

    /**
     * {@inheritdoc}
     */
    public function offsetSet($offset, $value)
    {
        throw new BadMethodCallException('List entries may not be changed.');
    }

    /**
     * {@inheritdoc}
     */
    public function offsetUnset($offset)
    {
        throw new BadMethodCallException('List entries may not be removed.');
    }

    /**
     * {@inheritdoc}
     */
    public function count()
    {
        return count($this->versions);
    }
}


================================================
FILE: src/Api/EditableRepository.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Api;

use InvalidArgumentException;
use Puli\Repository\Api\Resource\PuliResource;

/**
 * A repository that supports the addition and removal of resources.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
interface EditableRepository extends ResourceRepository
{
    /**
     * Adds a new resource to the repository.
     *
     * All resources passed to this method must implement {@link PuliResource}.
     *
     * @param string                          $path     The path at which to
     *                                                  add the resource.
     * @param PuliResource|ResourceCollection $resource The resource(s) to add
     *                                                  at that path.
     *
     * @throws InvalidArgumentException     If the path is invalid. The path
     *                                      must be  a non-empty string starting
     *                                      with "/".
     * @throws UnsupportedResourceException If the resource is invalid.
     */
    public function add($path, $resource);

    /**
     * Removes all resources matching the given query.
     *
     * @param string $query    A resource query.
     * @param string $language The language of the query. All implementations
     *                         must support the language "glob".
     *
     * @return int The number of resources removed from the repository.
     *
     * @throws InvalidArgumentException     If the query is invalid.
     * @throws UnsupportedLanguageException If the language is not supported.
     */
    public function remove($query, $language = 'glob');

    /**
     * Removes all resources from the repository.
     *
     * @return int The number of resources removed from the repository.
     */
    public function clear();
}


================================================
FILE: src/Api/NoVersionFoundException.php
================================================
<?php

/*
 * This file is part of the vendor/project package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Api;

use Exception;

/**
 * Thrown when a change stream contains no version of a resource.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class NoVersionFoundException extends ResourceNotFoundException
{
    /**
     * Creates a new exception for a resource path.
     *
     * @param string         $path  The path which was not found.
     * @param Exception|null $cause The exception that caused this exception.
     *
     * @return static The created exception.
     */
    public static function forPath($path, Exception $cause = null)
    {
        return new static(sprintf(
            'Could not find any version of path %s.',
            $path
        ), 0, $cause);
    }
}


================================================
FILE: src/Api/Resource/BodyResource.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Api\Resource;

/**
 * A resource that contains a body.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
interface BodyResource extends PuliResource
{
    /**
     * Returns the body of the resource.
     *
     * @return string The resource body.
     */
    public function getBody();
}


================================================
FILE: src/Api/Resource/FilesystemResource.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Api\Resource;

/**
 * A resource associated to a file on the file system.
 *
 * The path of the file can be accessed with {@link getFilesystemPath}.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
interface FilesystemResource extends PuliResource
{
    /**
     * Returns the path on the file system.
     *
     * @return string|null The file system path or `null` if the resource has no
     *                     associated local file.
     */
    public function getFilesystemPath();
}


================================================
FILE: src/Api/Resource/PuliResource.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Api\Resource;

use Puli\Repository\Api\ChangeStream\VersionList;
use Puli\Repository\Api\ResourceCollection;
use Puli\Repository\Api\ResourceNotFoundException;
use Puli\Repository\Api\ResourceRepository;
use Serializable;

/**
 * A resource.
 *
 * Resources are objects which can be stored in a resource repository. Resources
 * have a path, under which they are stored in the repository.
 *
 * Depending on the implementation, resources may offer additional functionality:
 *
 *  * Resources that are similar to files in that they have a body and a size
 *    should implement {@link BodyResource}.
 *
 * Resources can be attached to a repository by calling {@link attachTo()}. They
 * can be detached again by calling {@link detach()}. Use {@link isAttached()}
 * to find out whether a resource is attached and {@link getRepository()} to
 * obtain the attached repository.
 *
 * You can create a reference to a resource by calling {@link createReference()}.
 * References can have different paths than the resource they are referencing.
 * Otherwise, they are identical to the referenced resource. Use
 * {@link isReference()} to check whether a resource is a reference. You can
 * call {@link getRepositoryPath()} to retrieve the path of the referenced
 * resource.
 *
 * If you implement a custom resource, let your test extend
 * {@link AbstractResourceTest} to make sure your resource satisfies the
 * constraints of the interface. Extend {@link GenericResource} if you want to
 * avoid reimplementing basic functionality.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
interface PuliResource extends Serializable
{
    /**
     * Returns the path of the resource.
     *
     * For references created with {@link createReference()}, the path returned
     * by this method is the reference path and not the actual repository path
     * of the referenced resource. You should use {@link getRepositoryPath()} if
     * you want to glob the repository for a resource.
     *
     * @return string|null The path of the resource. If the resource has no
     *                     path, `null` is returned.
     */
    public function getPath();

    /**
     * Returns the name of the resource.
     *
     * The name is the last segment of the path returned by {@link getPath()}.
     *
     * @return string|null The name of the resource. If the resource has no
     *                     path, `null` is returned.
     */
    public function getName();

    /**
     * Returns the child resource with the given relative path.
     *
     * "." and ".." are supported as paths.
     *
     * @param string $relPath The relative resource path.
     *
     * @return PuliResource The resource with the given path.
     *
     * @throws ResourceNotFoundException If the resource cannot be found.
     */
    public function getChild($relPath);

    /**
     * Returns whether the child resource with the given relative path exists.
     *
     * @param string $relPath The relative resource path.
     *
     * @return bool Whether a resource with the given path exists.
     */
    public function hasChild($relPath);

    /**
     * Returns whether the resource has child resources.
     *
     * @return bool Returns `true` if the resource has child resources.
     */
    public function hasChildren();

    /**
     * Lists the child resources of the resources.
     *
     * @return ResourceCollection The child resources indexed by their names.
     */
    public function listChildren();

    /**
     * Returns the versions of this resource.
     *
     * @return VersionList The resource versions.
     */
    public function getVersions();

    /**
     * Returns metadata about a resource.
     *
     * @return ResourceMetadata The resource metadata.
     */
    public function getMetadata();

    /**
     * Returns the repository that the resource is attached to.
     *
     * Use {@link attachTo()} to attach a resource to a repository. The method
     * {@link detach()} can be used to detach an attached resource.
     *
     * @return ResourceRepository|null The resource repository. If the resource
     *                                 is not attached to any repository, `null`
     *                                 is returned.
     */
    public function getRepository();

    /**
     * Returns the path of the resource in the repository.
     *
     * The repository path is the path that the resource is mapped to once
     * attached to a repository. The result of this method is different from
     * {@link getPath()} for resource references. PuliResource references return
     * the path of the referenced resource here, while {@link getPath()} returns
     * the path of the reference itself.
     *
     * @return string|null The repository path of the resource. If the resource
     *                     has no repository path, `null` is returned.
     */
    public function getRepositoryPath();

    /**
     * Attaches the resource to a repository.
     *
     * You can optionally change the path of the resource by passing it in the
     * second argument. Beware that this may break the resource if it is still
     * referenced by another repository. Hence you should clone resources that
     * are attached to another repository before attaching them:
     *
     * ```php
     * if ($resource->isAttached()) {
     *     $resource = clone $resource;
     * }
     *
     * $resource->attachTo($repo, '/path/in/repo');
     * ```
     *
     * @param ResourceRepository $repo The repository.
     * @param string|null        $path The path of the resource in the
     *                                 repository. If not passed, the resource
     *                                 will be attached to it current path.
     */
    public function attachTo(ResourceRepository $repo, $path = null);

    /**
     * Detaches the resource from the repository.
     *
     * After calling this method, {@link isAttached()} returns `false`. The
     * method {@link getRepository()} should return `null` after detaching.
     *
     * Neither the path nor the repository path of the resource should be
     * modified when detaching.
     */
    public function detach();

    /**
     * Returns whether the resource is attached to a repository.
     *
     * Resources can be attached to a repository with {@link attachTo()}. The
     * method {@link getRepository()} returns the attached repository.
     *
     * @return bool Whether the resource is attached to a repository.
     */
    public function isAttached();

    /**
     * Creates a reference to the resource.
     *
     * References are identical for their referenced resource except for their
     * path. The path of the referenced resource can be obtained by calling
     * {@link getRepositoryPath()}:
     *
     * ```php
     * $resource = new MyResource('/path');
     * $reference = $resource->createReference('/reference');
     *
     * $reference->getPath();
     * // "/reference"
     *
     * $reference->getRepositoryPath();
     * // "/path"
     * ```
     *
     * Use {@link isReference()} to find out whether a resource is a reference.
     *
     * @param string $path The path of the reference.
     *
     * @return static The reference.
     */
    public function createReference($path);

    /**
     * Returns whether a resource is a reference.
     *
     * References are created by calling {@link createReference()}.
     *
     * @return bool Whether the resource is a reference.
     */
    public function isReference();
}


================================================
FILE: src/Api/Resource/ResourceMetadata.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Api\Resource;

/**
 * Contains metadata about a resource.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class ResourceMetadata
{
    /**
     * Returns when the resource was created.
     *
     * If this information is not available, the method returns 0.
     *
     * @return int A UNIX timestamp.
     */
    public function getCreationTime()
    {
        return 0;
    }

    /**
     * Returns when the resource was last accessed.
     *
     * If this information is not available, the method returns 0.
     *
     * @return int A UNIX timestamp.
     */
    public function getAccessTime()
    {
        return 0;
    }

    /**
     * Returns when the resource was last modified.
     *
     * If this information is not available, the method returns 0.
     *
     * @return int A UNIX timestamp.
     */
    public function getModificationTime()
    {
        return 0;
    }

    /**
     * Returns the size of the body in bytes.
     *
     * If this information is not available, the method returns 0.
     *
     * @return int The body size in bytes.
     */
    public function getSize()
    {
        return 0;
    }
}


================================================
FILE: src/Api/ResourceCollection.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Api;

use ArrayAccess;
use Countable;
use InvalidArgumentException;
use OutOfBoundsException;
use Puli\Repository\Api\Resource\PuliResource;
use Traversable;

/**
 * A collection of {@link PuliResource} instances.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
interface ResourceCollection extends Traversable, ArrayAccess, Countable
{
    /**
     * Adds a resource to the collection.
     *
     * @param PuliResource $resource The added resource.
     */
    public function add(PuliResource $resource);

    /**
     * Sets a resource at a collection key.
     *
     * @param int          $key      The collection key.
     * @param PuliResource $resource The resource to set.
     */
    public function set($key, PuliResource $resource);

    /**
     * Returns the resource for a collection key.
     *
     * @param int $key The collection key.
     *
     * @return PuliResource The resource at the key.
     *
     * @throws OutOfBoundsException If the key does not exist.
     */
    public function get($key);

    /**
     * Removes a collection key from the collection.
     *
     * @param int $key The collection key.
     */
    public function remove($key);

    /**
     * Returns whether a collection key exists.
     *
     * @param int $key The collection key.
     *
     * @return bool Whether the collection key exists.
     */
    public function has($key);

    /**
     * Removes all resources from the collection.
     */
    public function clear();

    /**
     * Returns the keys of the collection.
     *
     * @return int[] The collection keys.
     */
    public function keys();

    /**
     * Replaces the collection contents with the given resources.
     *
     * @param PuliResource[]|Traversable $resources The resources to write into
     *                                              the collection.
     *
     * @throws InvalidArgumentException     If the resources are not an array and
     *                                      not a traversable object.
     * @throws UnsupportedResourceException If a resource does not implement
     *                                      {@link PuliResource}.
     */
    public function replace($resources);

    /**
     * Merges the given resources into the collection.
     *
     * @param PuliResource[]|Traversable $resources The resources to merge into
     *                                              the collection.
     *
     * @throws InvalidArgumentException     If the resources are not an array
     *                                      and not a traversable object.
     * @throws UnsupportedResourceException If a resource does not implement
     *                                      {@link PuliResource}.
     */
    public function merge($resources);

    /**
     * Returns whether the collection is empty.
     *
     * @return bool Returns `true` only if the collection contains no resources.
     */
    public function isEmpty();

    /**
     * Returns the paths of all resources in the collection.
     *
     * The paths are returned in the order of their resources in the collection.
     *
     * @return string[] The paths of the resources in the collection.
     *
     * @see PuliResource::getPath
     */
    public function getPaths();

    /**
     * Returns the names of all resources in the collection.
     *
     * The names are returned in the order of their resources in the collection.
     *
     * @return string[] The names of the resources in the collection.
     *
     * @see PuliResource::getName
     */
    public function getNames();

    /**
     * Returns the collection contents as array.
     *
     * @return PuliResource[] The resources in the collection.
     */
    public function toArray();
}


================================================
FILE: src/Api/ResourceIterator.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Api;

use Iterator;
use Puli\Repository\Api\Resource\PuliResource;

/**
 * An iterator over {@link PuliResource} objects.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
interface ResourceIterator extends Iterator
{
    /**
     * Returns the resource at the current position of the iterator.
     *
     * @return PuliResource The resource at the current position.
     */
    public function getCurrentResource();
}


================================================
FILE: src/Api/ResourceNotFoundException.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Api;

use Exception;
use RuntimeException;

/**
 * Thrown when a requested resource was not found.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class ResourceNotFoundException extends RuntimeException
{
    /**
     * Creates a new exception for a resource path.
     *
     * @param string         $path  The path which was not found.
     * @param Exception|null $cause The exception that caused this exception.
     *
     * @return static The created exception.
     */
    public static function forPath($path, Exception $cause = null)
    {
        return new static(sprintf(
            'The resource %s does not exist.',
            $path
        ), 0, $cause);
    }
}


================================================
FILE: src/Api/ResourceRepository.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Api;

use InvalidArgumentException;
use Puli\Repository\Api\ChangeStream\VersionList;
use Puli\Repository\Api\Resource\PuliResource;

/**
 * Stores {@link PuliResource} objects.
 *
 * A resource repository is similar to a filesystem. It stores {@link PuliResource}
 * objects, each of which has a path in the repository:
 *
 * ```php
 * $resource = $repo->get('/css/style.css');
 * ```
 *
 * Resources may have child resources. These can be accessed with
 * {@link listChildren()}:
 *
 * ```php
 * $resource = $repo->get('/css');
 *
 * foreach ($resource->listChildren() as $name => $resource) {
 *     // ...
 * }
 * ```
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 * @author Titouan Galopin <galopintitouan@gmail.com>
 */
interface ResourceRepository
{
    /**
     * Returns the resource at the given path.
     *
     * @param string $path The path to the resource. Must start with "/". "."
     *                     and ".." segments in the path are supported.
     *
     * @return PuliResource The resource at this path.
     *
     * @throws ResourceNotFoundException If the resource cannot be found.
     * @throws InvalidArgumentException  If the path is invalid. The path must
     *                                   be a non-empty string starting with "/".
     */
    public function get($path);

    /**
     * Returns all versions of a resource.
     *
     * @param string $path The path to the resource.
     *
     * @return VersionList The versions stored for this path.
     *
     * @throws NoVersionFoundException  If no version can be found.
     * @throws InvalidArgumentException If the path is invalid. The path must
     *                                  be a non-empty string starting with "/".
     */
    public function getVersions($path);

    /**
     * Returns the resources matching a query.
     *
     * @param string $query    A resource query.
     * @param string $language The language of the query. All implementations
     *                         must support the language "glob".
     *
     * @return ResourceCollection The resources matching the query.
     *
     * @throws InvalidArgumentException     If the query is invalid.
     * @throws UnsupportedLanguageException If the language is not supported.
     */
    public function find($query, $language = 'glob');

    /**
     * Returns whether any resources match a query.
     *
     * @param string $query    A resource query.
     * @param string $language The language of the query. All implementations
     *                         must support the language "glob".
     *
     * @return bool Returns `true` if any resources exist that match the query.
     *
     * @throws InvalidArgumentException     If the query is invalid.
     * @throws UnsupportedLanguageException If the language is not supported.
     */
    public function contains($query, $language = 'glob');

    /**
     * Returns whether a resource has child resources.
     *
     * @param string $path The path to the resource. Must start with "/".
     *                     "." and ".." segments in the path are supported.
     *
     * @return bool Returns `true` if the resource has child resources.
     *
     * @throws ResourceNotFoundException If the resource cannot be found.
     * @throws InvalidArgumentException  If the path is invalid. The path must
     *                                   be a non-empty string starting with "/".
     */
    public function hasChildren($path);

    /**
     * Lists the child resources of a resource.
     *
     * @param string $path The path to the resource. Must start with "/".
     *                     "." and ".." segments in the path are supported.
     *
     * @return ResourceCollection The child resources of the resource.
     *
     * @throws ResourceNotFoundException If the resource cannot be found.
     * @throws InvalidArgumentException  If the path is invalid. The path must
     *                                   be a non-empty string starting with "/".
     */
    public function listChildren($path);
}


================================================
FILE: src/Api/UnsupportedLanguageException.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Api;

use Exception;
use RuntimeException;

/**
 * Thrown when a glob language is not supported by the repository.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class UnsupportedLanguageException extends RuntimeException
{
    /**
     * Creates an exception for an unsupported language string.
     *
     * @param string         $language The unsupported language.
     * @param Exception|null $cause    The exception that caused this exception.
     *
     * @return static The created exception.
     */
    public static function forLanguage($language, Exception $cause = null)
    {
        return new static(sprintf(
            'The language "%s" is not supported.',
            $language
        ), 0, $cause);
    }
}


================================================
FILE: src/Api/UnsupportedOperationException.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Api;

use RuntimeException;

/**
 * Thrown when the requested operation is not supported by the repository.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class UnsupportedOperationException extends RuntimeException
{
}


================================================
FILE: src/Api/UnsupportedResourceException.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Api;

use RuntimeException;

/**
 * Thrown when a specific implementation of {@link Resource\PuliResource} is not accepted by
 * the invoked method.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class UnsupportedResourceException extends RuntimeException
{
}


================================================
FILE: src/ChangeStream/InMemoryChangeStream.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\ChangeStream;

use Puli\Repository\Api\ChangeStream\ChangeStream;
use Puli\Repository\Api\ChangeStream\VersionList;
use Puli\Repository\Api\NoVersionFoundException;
use Puli\Repository\Api\Resource\PuliResource;
use Puli\Repository\Api\ResourceRepository;

/**
 * A change stream stored in memory.
 *
 * @since  1.0
 *
 * @author Titouan Galopin <galopintitouan@gmail.com>
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class InMemoryChangeStream implements ChangeStream
{
    /**
     * @var array
     */
    private $versions = array();

    /**
     * {@inheritdoc}
     */
    public function append(PuliResource $resource)
    {
        if (!isset($this->versions[$resource->getPath()])) {
            $this->versions[$resource->getPath()] = array();
        }

        $this->versions[$resource->getPath()][] = $resource;
    }

    /**
     * {@inheritdoc}
     */
    public function purge($path)
    {
        unset($this->versions[$path]);
    }

    /**
     * {@inheritdoc}
     */
    public function clear()
    {
        $this->versions = array();
    }

    /**
     * {@inheritdoc}
     */
    public function contains($path)
    {
        return isset($this->versions[$path]);
    }

    /**
     * {@inheritdoc}
     */
    public function getVersions($path, ResourceRepository $repository = null)
    {
        if (!isset($this->versions[$path])) {
            throw NoVersionFoundException::forPath($path);
        }

        $versions = array();

        foreach ($this->versions[$path] as $resource) {
            if (null !== $repository) {
                $resource = clone $resource;
                $resource->attachTo($repository, $path);
            }

            $versions[] = $resource;
        }

        return new VersionList($path, $versions);
    }
}


================================================
FILE: src/ChangeStream/JsonChangeStream.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\ChangeStream;

use Puli\Repository\Api\ChangeStream\ChangeStream;
use Puli\Repository\Api\ChangeStream\VersionList;
use Puli\Repository\Api\NoVersionFoundException;
use Puli\Repository\Api\Resource\PuliResource;
use Puli\Repository\Api\ResourceRepository;
use Webmozart\Json\JsonDecoder;
use Webmozart\Json\JsonEncoder;

/**
 * A change stream backed by a JSON file.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class JsonChangeStream implements ChangeStream
{
    /**
     * @var string
     */
    private $path;

    /**
     * @var array
     */
    private $json;

    /**
     * @var JsonEncoder
     */
    private $encoder;

    /**
     * @param string $path The path to the JSON file.
     */
    public function __construct($path)
    {
        $this->path = $path;
        $this->encoder = new JsonEncoder();
        $this->encoder->setPrettyPrinting(true);
        $this->encoder->setEscapeSlash(false);
    }

    /**
     * {@inheritdoc}
     */
    public function append(PuliResource $resource)
    {
        if (null === $this->json) {
            $this->load();
        }

        if (!isset($this->json[$resource->getPath()])) {
            $this->json[$resource->getPath()] = array();
        }

        $this->json[$resource->getPath()][] = serialize($resource);

        $this->flush();
    }

    /**
     * {@inheritdoc}
     */
    public function purge($path)
    {
        if (null === $this->json) {
            $this->load();
        }

        unset($this->json[$path]);

        $this->flush();
    }

    /**
     * {@inheritdoc}
     */
    public function clear()
    {
        if (null === $this->json) {
            $this->load();
        }

        $this->json = array();

        $this->flush();
    }

    /**
     * {@inheritdoc}
     */
    public function contains($path)
    {
        if (null === $this->json) {
            $this->load();
        }

        return isset($this->json[$path]);
    }

    /**
     * {@inheritdoc}
     */
    public function getVersions($path, ResourceRepository $repository = null)
    {
        if (null === $this->json) {
            $this->load();
        }

        if (!isset($this->json[$path])) {
            throw NoVersionFoundException::forPath($path);
        }

        $versions = array();

        foreach ($this->json[$path] as $resource) {
            $resource = unserialize($resource);

            if (null !== $repository) {
                $resource->attachTo($repository, $path);
            }

            $versions[] = $resource;
        }

        return new VersionList($path, $versions);
    }

    /**
     * Loads the JSON file.
     */
    private function load()
    {
        $decoder = new JsonDecoder();
        $decoder->setObjectDecoding(JsonDecoder::ASSOC_ARRAY);

        $this->json = file_exists($this->path)
            ? $decoder->decodeFile($this->path)
            : array();
    }

    /**
     * Writes the JSON file.
     */
    private function flush()
    {
        $this->encoder->encodeFile($this->json, $this->path);
    }
}


================================================
FILE: src/ChangeStream/KeyValueStoreChangeStream.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\ChangeStream;

use Puli\Repository\Api\ChangeStream\ChangeStream;
use Puli\Repository\Api\ChangeStream\VersionList;
use Puli\Repository\Api\NoVersionFoundException;
use Puli\Repository\Api\Resource\PuliResource;
use Puli\Repository\Api\ResourceRepository;
use Webmozart\KeyValueStore\Api\KeyValueStore;

/**
 * A change stream backed by a key-value store.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class KeyValueStoreChangeStream implements ChangeStream
{
    /**
     * @var KeyValueStore
     */
    private $store;

    /**
     * @param KeyValueStore $store
     */
    public function __construct(KeyValueStore $store)
    {
        $this->store = $store;
    }

    /**
     * {@inheritdoc}
     */
    public function append(PuliResource $resource)
    {
        $versions = $this->store->get($resource->getPath(), array());

        $versions[] = $resource;

        $this->store->set($resource->getPath(), $versions);
    }

    /**
     * {@inheritdoc}
     */
    public function purge($path)
    {
        $this->store->remove($path);
    }

    /**
     * {@inheritdoc}
     */
    public function contains($path)
    {
        return $this->store->exists($path);
    }

    /**
     * {@inheritdoc}
     */
    public function clear()
    {
        $this->store->clear();
    }

    /**
     * {@inheritdoc}
     */
    public function getVersions($path, ResourceRepository $repository = null)
    {
        $versions = $this->store->get($path, array());

        if (empty($versions)) {
            throw NoVersionFoundException::forPath($path);
        }

        if (null !== $repository) {
            foreach ($versions as $key => $resource) {
                $resource = clone $resource;
                $resource->attachTo($repository);
                $versions[$key] = $resource;
            }
        }

        return new VersionList($path, $versions);
    }
}


================================================
FILE: src/Discovery/ResourceBinding.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Discovery;

use Puli\Discovery\Api\Binding\Binding;
use Puli\Discovery\Api\Binding\Initializer\NotInitializedException;
use Puli\Discovery\Api\Type\MissingParameterException;
use Puli\Discovery\Api\Type\NoSuchParameterException;
use Puli\Discovery\Binding\AbstractBinding;
use Puli\Repository\Api\ResourceCollection;
use Puli\Repository\Api\ResourceRepository;

/**
 * Binds one or more resources to a binding type.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class ResourceBinding extends AbstractBinding
{
    /**
     * @var string
     */
    private $query;

    /**
     * @var string
     */
    private $language;

    /**
     * @var ResourceRepository
     */
    private $repo;

    /**
     * Creates a new resource binding.
     *
     * A resource binding has a query that is used to retrieve the resources
     * matched by the binding.
     *
     * You can pass parameters that have been defined for the type. If you pass
     * unknown parameters, or if a required parameter is missing, an exception
     * is thrown.
     *
     * All parameters that you do not set here will receive the default values
     * set for the parameter.
     *
     * @param string $query           The resource query.
     * @param string $typeName        The type to bind against.
     * @param array  $parameterValues The values of the parameters defined
     *                                for the type.
     * @param string $language        The language of the resource query.
     *
     * @throws NoSuchParameterException  If an invalid parameter was passed.
     * @throws MissingParameterException If a required parameter was not passed.
     */
    public function __construct($query, $typeName, array $parameterValues = array(), $language = 'glob')
    {
        parent::__construct($typeName, $parameterValues);

        $this->query = $query;
        $this->language = $language;
    }

    /**
     * Returns the query for the resources of the binding.
     *
     * @return string The resource query.
     */
    public function getQuery()
    {
        return $this->query;
    }

    /**
     * Returns the language of the query.
     *
     * @return string The query language.
     */
    public function getLanguage()
    {
        return $this->language;
    }

    /**
     * Returns the bound resources.
     *
     * @return ResourceCollection The bound resources.
     */
    public function getResources()
    {
        if (null === $this->repo) {
            throw new NotInitializedException('The repository of the resource binding must be set before accessing resources.');
        }

        return $this->repo->find($this->query, $this->language);
    }

    /**
     * Sets the repository used to load resources.
     *
     * @param ResourceRepository $repo The resource repository.
     */
    public function setRepository(ResourceRepository $repo)
    {
        $this->repo = $repo;
    }

    /**
     * {@inheritdoc}
     */
    public function equals(Binding $other)
    {
        if (!parent::equals($other)) {
            return false;
        }

        /** @var ResourceBinding $other */
        if ($this->query !== $other->query) {
            return false;
        }

        return $this->language === $other->language;
    }

    /**
     * {@inheritdoc}
     */
    protected function preSerialize(array &$data)
    {
        parent::preSerialize($data);

        $data[] = $this->query;
        $data[] = $this->language;
    }

    /**
     * {@inheritdoc}
     */
    protected function postUnserialize(array &$data)
    {
        $this->language = array_pop($data);
        $this->query = array_pop($data);

        parent::postUnserialize($data);
    }
}


================================================
FILE: src/Discovery/ResourceBindingInitializer.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Discovery;

use Puli\Discovery\Api\Binding\Binding;
use Puli\Discovery\Api\Binding\Initializer\BindingInitializer;
use Puli\Repository\Api\ResourceRepository;
use Webmozart\Assert\Assert;

/**
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class ResourceBindingInitializer implements BindingInitializer
{
    /**
     * @var ResourceRepository
     */
    private $repo;

    /**
     * @param ResourceRepository $repo
     */
    public function __construct(ResourceRepository $repo)
    {
        $this->repo = $repo;
    }

    /**
     * {@inheritdoc}
     */
    public function acceptsBinding($binding)
    {
        return $binding instanceof ResourceBinding
            || $binding === 'Puli\Repository\Discovery\ResourceBinding'
            || is_subclass_of($binding, 'Puli\Repository\Discovery\ResourceBinding');
    }

    /**
     * {@inheritdoc}
     */
    public function getAcceptedBindingClass()
    {
        return 'Puli\Repository\Discovery\ResourceBinding';
    }

    /**
     * {@inheritdoc}
     */
    public function initializeBinding(Binding $binding)
    {
        Assert::isInstanceOf($binding, 'Puli\Repository\Discovery\ResourceBinding', 'The binding must be an instance of ResourceBinding. Got: %s');

        /* @var ResourceBinding $binding */
        $binding->setRepository($this->repo);
    }
}


================================================
FILE: src/FilesystemRepository.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository;

use Iterator;
use Puli\Repository\Api\ChangeStream\ChangeStream;
use Puli\Repository\Api\Resource\BodyResource;
use Puli\Repository\Api\Resource\FilesystemResource;
use Puli\Repository\Api\Resource\PuliResource;
use Puli\Repository\Api\ResourceCollection;
use Puli\Repository\Api\ResourceNotFoundException;
use Puli\Repository\Api\UnsupportedOperationException;
use Puli\Repository\Api\UnsupportedResourceException;
use Puli\Repository\Resource\Collection\FilesystemResourceCollection;
use Puli\Repository\Resource\DirectoryResource;
use Puli\Repository\Resource\FileResource;
use Puli\Repository\Resource\LinkResource;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
use Webmozart\Assert\Assert;
use Webmozart\Glob\Iterator\GlobIterator;
use Webmozart\Glob\Iterator\RecursiveDirectoryIterator;
use Webmozart\PathUtil\Path;

/**
 * A repository reading from the file system.
 *
 * Resources can be read using their absolute file system paths:
 *
 * ```php
 * use Puli\Repository\FilesystemRepository;
 *
 * $repo = new FilesystemRepository();
 * $resource = $repo->get('/home/puli/.gitconfig');
 * ```
 *
 * The returned resources implement {@link FilesystemResource}.
 *
 * Optionally, a root directory can be passed to the constructor. Then all paths
 * will be read relative to that directory:
 *
 * ```php
 * $repo = new FilesystemRepository('/home/puli');
 * $resource = $repo->get('/.gitconfig');
 * ```
 *
 * While "." and ".." segments are supported, files outside the root directory
 * cannot be read. Any leading ".." segments will simply be stripped off.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class FilesystemRepository extends AbstractEditableRepository
{
    /**
     * @var bool|null
     */
    private static $symlinkSupported;

    /**
     * @var string
     */
    private $baseDir;

    /**
     * @var int
     */
    private $baseDirLength;

    /**
     * @var bool
     */
    private $symlink;

    /**
     * @var bool
     */
    private $relative;

    /**
     * @var Filesystem
     */
    private $filesystem;

    /**
     * Returns whether symlinks are supported in the local environment.
     *
     * @return bool Returns `true` if symlinks are supported.
     */
    public static function isSymlinkSupported()
    {
        if (null === self::$symlinkSupported) {
            // http://php.net/manual/en/function.symlink.php
            // Symlinks are only supported on Windows Vista, Server 2008 or
            // greater on PHP 5.3+
            if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
                self::$symlinkSupported = PHP_WINDOWS_VERSION_MAJOR >= 6;
            } else {
                self::$symlinkSupported = true;
            }
        }

        return self::$symlinkSupported;
    }

    /**
     * Creates a new repository.
     *
     * @param string            $baseDir      The base directory of the repository on the file
     *                                        system.
     * @param bool              $symlink      Whether to use symbolic links for added files. If
     *                                        symbolic links are not supported on the current
     *                                        system, the repository will create hard copies
     *                                        instead.
     * @param bool              $relative     Whether to create relative symbolic links. If
     *                                        relative links are not supported on the current
     *                                        system, the repository will create absolute links
     *                                        instead.
     * @param ChangeStream|null $changeStream If provided, the repository will log
     *                                        resources changes in this change stream.
     */
    public function __construct($baseDir = '/', $symlink = true, $relative = true, ChangeStream $changeStream = null)
    {
        parent::__construct($changeStream);

        Assert::directory($baseDir);
        Assert::boolean($symlink);

        $this->baseDir = rtrim(Path::canonicalize($baseDir), '/');
        $this->baseDirLength = strlen($baseDir);
        $this->symlink = $symlink && self::isSymlinkSupported();
        $this->relative = $this->symlink && $relative;
        $this->filesystem = new Filesystem();
    }

    /**
     * {@inheritdoc}
     */
    public function get($path)
    {
        $path = $this->sanitizePath($path);
        $filesystemPath = $this->baseDir.$path;

        if (!file_exists($filesystemPath)) {
            throw ResourceNotFoundException::forPath($path);
        }

        return $this->createResource($filesystemPath, $path);
    }

    /**
     * {@inheritdoc}
     */
    public function find($query, $language = 'glob')
    {
        return $this->iteratorToCollection($this->getGlobIterator($query, $language));
    }

    /**
     * {@inheritdoc}
     */
    public function contains($query, $language = 'glob')
    {
        $iterator = $this->getGlobIterator($query, $language);
        $iterator->rewind();

        return $iterator->valid();
    }

    /**
     * {@inheritdoc}
     */
    public function hasChildren($path)
    {
        $filesystemPath = $this->getFilesystemPath($path);

        if (!is_dir($filesystemPath)) {
            return false;
        }

        $iterator = $this->getDirectoryIterator($filesystemPath);
        $iterator->rewind();

        return $iterator->valid();
    }

    /**
     * {@inheritdoc}
     */
    public function listChildren($path)
    {
        $filesystemPath = $this->getFilesystemPath($path);

        if (!is_dir($filesystemPath)) {
            return new FilesystemResourceCollection();
        }

        return $this->iteratorToCollection($this->getDirectoryIterator($filesystemPath));
    }

    /**
     * {@inheritdoc}
     */
    public function add($path, $resource)
    {
        $path = $this->sanitizePath($path);

        if ($resource instanceof ResourceCollection) {
            $this->ensureDirectoryExists($path);
            foreach ($resource as $child) {
                $this->addResource($path.'/'.$child->getName(), $child);
            }

            return;
        }

        if ($resource instanceof PuliResource) {
            $this->ensureDirectoryExists(Path::getDirectory($path));
            $this->addResource($path, $resource);

            return;
        }

        throw new UnsupportedResourceException(sprintf(
            'The passed resource must be a PuliResource or ResourceCollection. Got: %s',
            is_object($resource) ? get_class($resource) : gettype($resource)
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function remove($query, $language = 'glob')
    {
        $iterator = $this->getGlobIterator($query, $language);
        $removed = 0;

        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');

        // There's some problem with concurrent deletions at the moment
        foreach (iterator_to_array($iterator) as $filesystemPath) {
            $this->removeResource($filesystemPath, $removed);
        }

        return $removed;
    }

    /**
     * {@inheritdoc}
     */
    public function clear()
    {
        $iterator = $this->getDirectoryIterator($this->baseDir);
        $removed = 0;

        // Batch-delete all versions
        $this->clearVersions();

        foreach ($iterator as $filesystemPath) {
            $this->removeResource($filesystemPath, $removed);
        }

        $this->storeVersion($this->get('/'));

        return $removed;
    }

    private function ensureDirectoryExists($path)
    {
        $filesystemPath = $this->baseDir.$path;

        if (is_file($filesystemPath)) {
            throw new UnsupportedOperationException(sprintf(
                'Instances of BodyResource do not support child resources in '.
                'FilesystemRepository. Tried to add a child to %s.',
                $filesystemPath
            ));
        }

        if (!is_dir($filesystemPath)) {
            mkdir($filesystemPath, 0777, true);
        }
    }

    private function addResource($path, PuliResource $resource, $checkParentsForSymlinks = true)
    {
        $pathInBaseDir = $this->baseDir.$path;
        $hasChildren = $resource->hasChildren();
        $hasBody = $resource instanceof BodyResource;

        if ($hasChildren && $hasBody) {
            throw new UnsupportedResourceException(sprintf(
                'Instances of BodyResource do not support child resources in '.
                'FilesystemRepository. Tried to add a BodyResource with '.
                'children at %s.',
                $path
            ));
        }

        $resource = clone $resource;
        $resource->attachTo($this, $path);

        if ($this->symlink && $checkParentsForSymlinks) {
            $this->replaceParentSymlinksByCopies($path);
        }

        if ($resource instanceof FilesystemResource) {
            if ($this->symlink) {
                $this->symlinkMirror($resource->getFilesystemPath(), $pathInBaseDir);
            } elseif ($hasBody) {
                $this->filesystem->copy($resource->getFilesystemPath(), $pathInBaseDir);
            } else {
                $this->filesystem->mirror($resource->getFilesystemPath(), $pathInBaseDir);
            }

            $this->storeVersion($resource);

            return;
        }

        if ($resource instanceof LinkResource) {
            if (!$this->symlink) {
                throw new UnsupportedResourceException(sprintf(
                    'LinkResource requires support of symbolic links in FilesystemRepository. '.
                    'Tried to add a LinkResource at %s.',
                    $path
                ));
            }

            $this->filesystem->symlink($this->baseDir.$resource->getTargetPath(), $pathInBaseDir);

            $this->storeVersion($resource);

            return;
        }

        if ($hasBody) {
            file_put_contents($pathInBaseDir, $resource->getBody());

            $this->storeVersion($resource);

            return;
        }

        if (is_file($pathInBaseDir)) {
            $this->filesystem->remove($pathInBaseDir);
        }

        if (!file_exists($pathInBaseDir)) {
            mkdir($pathInBaseDir, 0777, true);
        }

        foreach ($resource->listChildren() as $child) {
            $this->addResource($path.'/'.$child->getName(), $child, false);
        }

        $this->storeVersion($resource);
    }

    private function removeResource($filesystemPath, &$removed)
    {
        // Skip paths that have already been removed
        if (!file_exists($filesystemPath)) {
            return;
        }

        $this->removeVersions($this->getPath($filesystemPath));

        ++$removed;

        if (is_dir($filesystemPath) && !is_link($filesystemPath)) {
            $iterator = $this->getDirectoryIterator($filesystemPath);

            foreach ($iterator as $childFilesystemPath) {
                // Remove children and child versions
                $this->removeResource($childFilesystemPath, $removed);
            }
        }

        $this->filesystem->remove($filesystemPath);
    }

    private function createResource($filesystemPath, $path)
    {
        $resource = null;

        if (is_link($filesystemPath)) {
            $baseDir = rtrim($this->baseDir, '/');
            $targetFilesystemPath = $this->readLink($filesystemPath);

            if (Path::isBasePath($baseDir, $targetFilesystemPath)) {
                $targetPath = '/'.Path::makeRelative($targetFilesystemPath, $baseDir);
                $resource = new LinkResource($targetPath);
            }
        }

        if (!$resource && is_dir($filesystemPath)) {
            $resource = new DirectoryResource($filesystemPath);
        }

        if (!$resource) {
            $resource = new FileResource($filesystemPath);
        }

        $resource->attachTo($this, $path);

        return $resource;
    }

    private function iteratorToCollection(Iterator $iterator)
    {
        $filesystemPaths = iterator_to_array($iterator);
        $resources = array();

        // RecursiveDirectoryIterator is not guaranteed to return sorted results
        sort($filesystemPaths);

        foreach ($filesystemPaths as $filesystemPath) {
            $resource = is_dir($filesystemPath)
                ? new DirectoryResource($filesystemPath, $this->getPath($filesystemPath))
                : new FileResource($filesystemPath, $this->getPath($filesystemPath));

            $resource->attachTo($this);

            $resources[] = $resource;
        }

        return new FilesystemResourceCollection($resources);
    }

    private function getFilesystemPath($path)
    {
        $path = $this->sanitizePath($path);
        $filesystemPath = $this->baseDir.$path;

        if (!file_exists($filesystemPath)) {
            throw ResourceNotFoundException::forPath($path);
        }

        return $filesystemPath;
    }

    private function getGlobIterator($query, $language)
    {
        $this->failUnlessGlob($language);

        Assert::stringNotEmpty($query, 'The glob must be a non-empty string. Got: %s');
        Assert::startsWith($query, '/', 'The glob %s is not absolute.');

        $query = Path::canonicalize($query);

        return new GlobIterator($this->baseDir.$query);
    }

    private function getDirectoryIterator($filesystemPath)
    {
        return new RecursiveDirectoryIterator(
            $filesystemPath,
            RecursiveDirectoryIterator::CURRENT_AS_PATHNAME | RecursiveDirectoryIterator::SKIP_DOTS
        );
    }

    private function symlinkMirror($origin, $target, array $dirsToKeep = array())
    {
        $targetIsDir = is_dir($target);
        $forceDir = in_array($target, $dirsToKeep, true);

        // Merge directories
        if (is_dir($origin) && ($targetIsDir || $forceDir)) {
            if (is_link($target)) {
                $this->replaceLinkByCopy($target, $dirsToKeep);
            }

            $iterator = $this->getDirectoryIterator($origin);

            foreach ($iterator as $path) {
                $this->symlinkMirror($path, $target.'/'.basename($path), $dirsToKeep);
            }

            return;
        }

        // Replace target
        if (file_exists($target)) {
            $this->filesystem->remove($target);
        }

        // Try creating a relative link
        if ($this->relative && $this->trySymlink(Path::makeRelative($origin, Path::getDirectory($target)), $target)) {
            return;
        }

        // Try creating a absolute link
        if ($this->trySymlink($origin, $target)) {
            return;
        }

        // Fall back to copy
        if (is_dir($origin)) {
            $this->filesystem->mirror($origin, $target);

            return;
        }

        $this->filesystem->copy($origin, $target);
    }

    private function replaceParentSymlinksByCopies($path)
    {
        $previousPath = null;

        // Collect all paths that MUST NOT be symlinks after doing the
        // replace operation.
        //
        // Example:
        //
        // $dirsToKeep = ['/path/to/webmozart', '/path/to/webmozart/views']
        //
        // Before:
        //   /webmozart -> target
        //
        // After:
        //   /webmozart
        //     /config -> target/config
        //     /views
        //       /index.html.twig -> target/views/index.html.twig

        $dirsToKeep = array();

        while ($previousPath !== ($path = Path::getDirectory($path))) {
            $filesystemPath = $this->baseDir.$path;
            $dirsToKeep[] = $filesystemPath;

            if (is_link($filesystemPath)) {
                $this->replaceLinkByCopy($filesystemPath, $dirsToKeep);

                return;
            }

            $previousPath = $path;
        }
    }

    private function replaceLinkByCopy($path, array $dirsToKeep = array())
    {
        $target = Path::makeAbsolute($this->readLink($path), Path::getDirectory($path));
        $this->filesystem->remove($path);
        $this->filesystem->mkdir($path);
        $this->symlinkMirror($target, $path, $dirsToKeep);
    }

    private function trySymlink($origin, $target)
    {
        try {
            $this->filesystem->symlink($origin, $target);

            if (file_exists($target)) {
                return true;
            }
        } catch (IOException $e) {
        }

        return false;
    }

    private function readLink($filesystemPath)
    {
        // On Windows, transitive links are resolved to the final target by
        // readlink(). realpath(), however, returns the target link on Windows,
        // but not on Unix.

        // /link1 -> /link2 -> /file

        // Windows: readlink(/link1) => /file
        //          realpath(/link1) => /link2

        // Unix:    readlink(/link1) => /link2
        //          realpath(/link1) => /file

        // Consistency FTW!

        return '\\' === DIRECTORY_SEPARATOR ? realpath($filesystemPath) : readlink($filesystemPath);
    }

    private function getPath($filesystemPath)
    {
        return substr($filesystemPath, $this->baseDirLength);
    }
}


================================================
FILE: src/InMemoryRepository.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository;

use ArrayIterator;
use Puli\Repository\Api\ChangeStream\ChangeStream;
use Puli\Repository\Api\Resource\PuliResource;
use Puli\Repository\Api\ResourceCollection;
use Puli\Repository\Api\ResourceNotFoundException;
use Puli\Repository\Api\UnsupportedResourceException;
use Puli\Repository\Resource\Collection\ArrayResourceCollection;
use Puli\Repository\Resource\GenericResource;
use Webmozart\Assert\Assert;
use Webmozart\Glob\Glob;
use Webmozart\Glob\Iterator\GlobFilterIterator;
use Webmozart\Glob\Iterator\RegexFilterIterator;
use Webmozart\PathUtil\Path;

/**
 * An in-memory resource repository.
 *
 * Resources can be added with the method {@link add()}:
 *
 * ```php
 * use Puli\Repository\InMemoryRepository;
 *
 * $repo = new InMemoryRepository();
 * $repo->add('/css', new DirectoryResource('/path/to/project/res/css'));
 * ```
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class InMemoryRepository extends AbstractEditableRepository
{
    /**
     * @var PuliResource[]
     */
    private $resources = array();

    /**
     * Create the repository.
     *
     * @param ChangeStream|null $changeStream If provided, the repository will log
     *                                        resources changes in this change stream.
     */
    public function __construct(ChangeStream $changeStream = null)
    {
        parent::__construct($changeStream);

        $this->clear();
    }

    /**
     * {@inheritdoc}
     */
    public function get($path)
    {
        $path = $this->sanitizePath($path);

        if (!isset($this->resources[$path])) {
            throw ResourceNotFoundException::forPath($path);
        }

        return $this->resources[$path];
    }

    /**
     * {@inheritdoc}
     */
    public function find($query, $language = 'glob')
    {
        $this->failUnlessGlob($language);

        $query = $this->sanitizePath($query);
        $resources = array();

        if (Glob::isDynamic($query)) {
            $resources = $this->getGlobIterator($query);
        } elseif (isset($this->resources[$query])) {
            $resources = array($this->resources[$query]);
        }

        return new ArrayResourceCollection($resources);
    }

    /**
     * {@inheritdoc}
     */
    public function contains($query, $language = 'glob')
    {
        $this->failUnlessGlob($language);

        $query = $this->sanitizePath($query);

        if (Glob::isDynamic($query)) {
            $iterator = $this->getGlobIterator($query);
            $iterator->rewind();

            return $iterator->valid();
        }

        return isset($this->resources[$query]);
    }

    /**
     * {@inheritdoc}
     */
    public function add($path, $resource)
    {
        $path = $this->sanitizePath($path);

        if ($resource instanceof ResourceCollection) {
            $this->ensureDirectoryExists($path);
            foreach ($resource as $child) {
                $this->addResource($path.'/'.$child->getName(), $child);
            }

            // Keep the resources sorted by file name
            ksort($this->resources);

            return;
        }

        if ($resource instanceof PuliResource) {
            $this->ensureDirectoryExists(Path::getDirectory($path));
            $this->addResource($path, $resource);

            ksort($this->resources);

            return;
        }

        throw new UnsupportedResourceException(sprintf(
            'The passed resource must be a PuliResource or ResourceCollection. Got: %s',
            is_object($resource) ? get_class($resource) : gettype($resource)
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function remove($query, $language = 'glob')
    {
        $resources = $this->find($query, $language);
        $nbOfResources = count($this->resources);

        // Run the assertion after find(), so that we know that $query is valid
        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');

        foreach ($resources as $resource) {
            $this->removeResource($resource);
        }

        return $nbOfResources - count($this->resources);
    }

    /**
     * {@inheritdoc}
     */
    public function clear()
    {
        $root = new GenericResource('/');
        $root->attachTo($this);

        // Subtract root
        $removed = count($this->resources) - 1;

        $this->resources = array('/' => $root);

        $this->clearVersions();
        $this->storeVersion($root);

        return $removed;
    }

    /**
     * {@inheritdoc}
     */
    public function listChildren($path)
    {
        $iterator = $this->getChildIterator($this->get($path));

        return new ArrayResourceCollection($iterator);
    }

    /**
     * {@inheritdoc}
     */
    public function hasChildren($path)
    {
        $iterator = $this->getChildIterator($this->get($path));
        $iterator->rewind();

        return $iterator->valid();
    }

    /**
     * Recursively creates a directory for a path.
     *
     * @param string $path A directory path.
     */
    private function ensureDirectoryExists($path)
    {
        if (!isset($this->resources[$path])) {
            // Recursively initialize parent directories
            if ($path !== '/') {
                $this->ensureDirectoryExists(Path::getDirectory($path));
            }

            $this->resources[$path] = new GenericResource($path);
            $this->resources[$path]->attachTo($this);

            return;
        }
    }

    private function addResource($path, PuliResource $resource)
    {
        $basePath = '/' === $path ? $path : $path.'/';

        // Read children before attaching the resource to this repository
        $children = $resource->listChildren();

        $resource = clone $resource;
        $resource->attachTo($this, $path);

        // Add the resource before adding its children, so that the array
        // stays sorted
        $this->resources[$path] = $resource;

        foreach ($children as $name => $child) {
            $this->addResource($basePath.$name, $child);
        }

        $this->storeVersion($resource);
    }

    private function removeResource(PuliResource $resource)
    {
        $path = $resource->getPath();

        $this->removeVersions($path);

        // Ignore non-existing resources
        if (!isset($this->resources[$path])) {
            return;
        }

        // Recursively remove directory contents
        foreach ($this->getChildIterator($resource) as $child) {
            $this->removeResource($child);
        }

        unset($this->resources[$path]);

        // Detach from locator
        $resource->detach();
    }

    /**
     * Returns an iterator for the children of a resource.
     *
     * @param PuliResource $resource The resource.
     *
     * @return RegexFilterIterator The iterator.
     */
    private function getChildIterator(PuliResource $resource)
    {
        $staticPrefix = rtrim($resource->getPath(), '/').'/';
        $regExp = '~^'.preg_quote($staticPrefix, '~').'[^/]+$~';

        return new RegexFilterIterator(
            $regExp,
            $staticPrefix,
            new ArrayIterator($this->resources),
            RegexFilterIterator::FILTER_KEY
        );
    }

    /**
     * Returns an iterator for a glob.
     *
     * @param string $glob The glob.
     *
     * @return GlobFilterIterator The iterator.
     */
    protected function getGlobIterator($glob)
    {
        return new GlobFilterIterator(
            $glob,
            new ArrayIterator($this->resources),
            GlobFilterIterator::FILTER_KEY
        );
    }
}


================================================
FILE: src/JsonRepository.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository;

use InvalidArgumentException;
use Puli\Repository\Api\ChangeStream\VersionList;
use Puli\Repository\Api\EditableRepository;
use Puli\Repository\Api\NoVersionFoundException;
use Puli\Repository\Api\Resource\PuliResource;
use RecursiveIteratorIterator;
use Webmozart\Glob\Glob;
use Webmozart\Glob\Iterator\RecursiveDirectoryIterator;
use Webmozart\PathUtil\Path;

/**
 * A repository backed by a JSON file optimized for development.
 *
 * The generated JSON file is described by res/schema/repository-schema-1.0.json.
 *
 * Resources can be added with the method {@link add()}:
 *
 * ```php
 * use Puli\Repository\JsonRepository;
 *
 * $repo = new JsonRepository('/path/to/repository.json', '/path/to/project');
 * $repo->add('/css', new DirectoryResource('/path/to/project/res/css'));
 * ```
 *
 * When adding a resource, the added filesystem path is stored in the JSON file
 * under the key of the Puli path. The path is stored relatively to the base
 * directory passed to the constructor:
 *
 * ```json
 * {
 *     "/css": "res/css"
 * }
 * ```
 *
 * Mapped resources can be read with the method {@link get()}:
 *
 * ```php
 * $cssPath = $repo->get('/css')->getFilesystemPath();
 * ```
 *
 * You can also access nested files:
 *
 * ```php
 * echo $repo->get('/css/style.css')->getBody();
 * ```
 *
 * Nested files are searched during {@link get()}. As a consequence, this
 * implementation should not be used in production environments. Use
 * {@link OptimizedJsonRepository} instead which searches nested files during
 * {@link add()} and has much faster read performance.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 * @author Titouan Galopin <galopintitouan@gmail.com>
 */
class JsonRepository extends AbstractJsonRepository implements EditableRepository
{
    /**
     * Flag: Don't search the contents of mapped directories for matching paths.
     *
     * @internal
     */
    const NO_SEARCH_FILESYSTEM = 2;

    /**
     * Flag: Don't filter out references that don't exist on the filesystem.
     *
     * @internal
     */
    const NO_CHECK_FILE_EXISTS = 4;

    /**
     * Flag: Include the references for mapped ancestor paths /a of a path /a/b.
     *
     * @internal
     */
    const INCLUDE_ANCESTORS = 8;

    /**
     * Flag: Include the references for mapped nested paths /a/b of a path /a.
     *
     * @internal
     */
    const INCLUDE_NESTED = 16;

    /**
     * Creates a new repository.
     *
     * @param string $path          The path to the JSON file. If relative, it
     *                              must be relative to the base directory.
     * @param string $baseDirectory The base directory of the store. Paths
     *                              inside that directory are stored as relative
     *                              paths. Paths outside that directory are
     *                              stored as absolute paths.
     * @param bool   $validateJson  Whether to validate the JSON file against
     *                              the schema. Slow but spots problems.
     */
    public function __construct($path, $baseDirectory, $validateJson = false)
    {
        // Does not accept ChangeStream objects
        // The ChangeStream functionality is implemented by the repository itself
        parent::__construct($path, $baseDirectory, $validateJson);
    }

    /**
     * {@inheritdoc}
     */
    public function getVersions($path)
    {
        if (!$this->json) {
            $this->load();
        }

        $references = $this->searchReferences($path);

        if (!isset($references[$path])) {
            throw NoVersionFoundException::forPath($path);
        }

        $resources = array();
        $pathReferences = $references[$path];

        // The first reference is the last (current) version
        // Hence traverse in reverse order
        for ($ref = end($pathReferences); null !== key($pathReferences); $ref = prev($pathReferences)) {
            $resources[] = $this->createResource($path, $ref);
        }

        return new VersionList($path, $resources);
    }

    /**
     * {@inheritdoc}
     */
    protected function storeVersion(PuliResource $resource)
    {
        $path = $resource->getPath();

        // Newly inserted parent directories and the resource need to be
        // sorted before we can correctly search references below
        krsort($this->json);

        // If a mapping exists for a sub-path of this resource
        // (e.g. $path = /a, mapped sub-path = /a/b)
        // we need to record the order, since by default sub-paths are
        // preferred over super paths

        $references = $this->searchReferences(
            $path,
            // Don't do filesystem checks here. We only check the filesystem
            // when reading, not when adding.
            self::NO_SEARCH_FILESYSTEM | self::NO_CHECK_FILE_EXISTS
                // Include references for mapped ancestor and nested paths
                | self::INCLUDE_ANCESTORS | self::INCLUDE_NESTED
        );

        // Filter virtual resources
        $references = array_filter($references, function ($currentReferences) {
            return array(null) !== $currentReferences;
        });

        // The $references contain:
        // - any sub references (e.g. /a/b/c, /a/b/d)
        // - the reference itself at $pos (e.g. /a/b)
        // - non-null parent references (e.g. /a)
        // (in that order, since long paths are sorted before short paths)
        $pos = array_search($path, array_keys($references), true);

        // We need to do three things:

        // 1. If any parent mapping has an order defined, inherit that order

        if ($pos + 1 < count($references)) {
            // Inherit the parent order if necessary
            if (!isset($this->json['_order'][$path])) {
                $parentReferences = array_slice($references, $pos + 1);

                $this->initWithParentOrder($path, $parentReferences);
            }

            // A parent order was inherited. Insert the path itself.
            if (isset($this->json['_order'][$path])) {
                $this->prependOrderEntry($path, $path);
            }
        }

        // 2. If there are child mappings, insert the current path into their order

        if ($pos > 0) {
            $subReferences = array_slice($references, 0, $pos);

            foreach ($subReferences as $subPath => $_) {
                if (isset($this->json['_order'][$subPath])) {
                    continue;
                }

                if (isset($this->json['_order'][$path])) {
                    $this->json['_order'][$subPath] = $this->json['_order'][$path];
                } else {
                    $this->initWithDefaultOrder($subPath, $path, $references);
                }
            }

            // After initializing all order entries, insert the new one
            foreach ($subReferences as $subPath => $_) {
                $this->prependOrderEntry($subPath, $path);
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function insertReference($path, $reference)
    {
        if (!isset($this->json[$path])) {
            // Store first entries as simple reference
            $this->json[$path] = $reference;

            return;
        }

        if ($reference === $this->json[$path]) {
            // Reference is already set
            return;
        }

        if (!is_array($this->json[$path])) {
            // Convert existing entries to arrays for follow ups
            $this->json[$path] = array($this->json[$path]);
        }

        if (!in_array($reference, $this->json[$path], true)) {
            // Insert at the beginning of the array
            array_unshift($this->json[$path], $reference);
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function removeReferences($glob)
    {
        $checkResults = $this->getReferencesForGlob($glob);
        $nonDeletablePaths = array();

        foreach ($checkResults as $path => $filesystemPath) {
            if (!array_key_exists($path, $this->json)) {
                $nonDeletablePaths[] = $filesystemPath;
            }
        }

        if (count($nonDeletablePaths) > 0) {
            throw new InvalidArgumentException(sprintf(
                'You cannot remove resources that are not mapped in the JSON '.
                'file. Tried to remove %s%s.',
                reset($nonDeletablePaths),
                count($nonDeletablePaths) > 1
                    ? ' and '.(count($nonDeletablePaths) - 1).' more'
                    : ''
            ));
        }

        $deletedPaths = $this->getReferencesForGlob($glob.'{,/**/*}', self::NO_SEARCH_FILESYSTEM);
        $removed = 0;

        foreach ($deletedPaths as $path => $filesystemPath) {
            $removed += 1 + count($this->getReferencesForGlob($path.'/**/*'));

            unset($this->json[$path]);
        }

        return $removed;
    }

    /**
     * {@inheritdoc}
     */
    protected function getReferencesForPath($path)
    {
        // Stop on first result and flatten
        return $this->flatten($this->searchReferences($path, self::STOP_ON_FIRST));
    }

    /**
     * {@inheritdoc}
     */
    protected function getReferencesForGlob($glob, $flags = 0)
    {
        if (!Glob::isDynamic($glob)) {
            return $this->getReferencesForPath($glob);
        }

        return $this->getReferencesForRegex(
            Glob::getBasePath($glob),
            Glob::toRegEx($glob),
            $flags
        );
    }

    /**
     * {@inheritdoc}
     */
    protected function getReferencesForRegex($staticPrefix, $regex, $flags = 0, $maxDepth = 0)
    {
        return $this->flattenWithFilter(
            // Never stop on the first result before applying the filter since
            // the filter may reject the only returned path
            $this->searchReferences($staticPrefix, self::INCLUDE_NESTED),
            $regex,
            $flags,
            $maxDepth
        );
    }

    /**
     * {@inheritdoc}
     */
    protected function getReferencesInDirectory($path, $flags = 0)
    {
        $basePath = rtrim($path, '/');

        return $this->getReferencesForRegex(
            $basePath.'/',
            '~^'.preg_quote($basePath, '~').'/[^/]+$~',
            $flags,
            // Limit the directory exploration to the depth of the path + 1
            $this->getPathDepth($path) + 1
        );
    }

    /**
     * Flattens a two-level reference array into a one-level array.
     *
     * For each entry on the first level, only the first entry of the second
     * level is included in the result.
     *
     * Each reference returned by this method can be:
     *
     *  * `null`
     *  * a link starting with `@`
     *  * an absolute filesystem path
     *
     * The keys of the returned array are Puli paths. Their order is undefined.
     *
     * @param array $references A two-level reference array as returned by
     *                          {@link searchReferences()}.
     *
     * @return string[]|null[] A one-level array of references with Puli paths
     *                         as keys.
     */
    private function flatten(array $references)
    {
        $result = array();

        foreach ($references as $currentPath => $currentReferences) {
            if (!isset($result[$currentPath])) {
                $result[$currentPath] = reset($currentReferences);
            }
        }

        return $result;
    }

    /**
     * Flattens a two-level reference array into a one-level array and filters
     * out any references that don't match the given regular expression.
     *
     * This method takes a two-level reference array as returned by
     * {@link searchReferences()}. The references are scanned for Puli paths
     * matching the given regular expression. Those matches are returned.
     *
     * If a matching path refers to more than one reference, the first reference
     * is returned in the resulting array.
     *
     * All references that contain directory paths may be traversed recursively and
     * scanned for more paths matching the regular expression. This recursive
     * traversal can be limited by passing a `$maxDepth` (see {@link getPathDepth()}).
     * By default, this `$maxDepth` is equal to zero (no recursive scan).
     *
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
     *
     * The flag `NO_SEARCH_FILESYSTEM` may be used to check for whether the found
     * paths actually exist on the filesystem.
     *
     * Each reference returned by this method can be:
     *
     *  * `null`
     *  * a link starting with `@`
     *  * an absolute filesystem path
     *
     * The keys of the returned array are Puli paths. Their order is undefined.
     *
     * @param array  $references A two-level reference array as returned by
     *                           {@link searchReferences()}.
     * @param string $regex      A regular expression used to filter Puli paths.
     * @param int    $flags      A bitwise combination of the flag constants in
     *                           this class.
     * @param int    $maxDepth   The maximum path depth when searching the
     *                           contents of directory references. If 0, the
     *                           depth is unlimited.
     *
     * @return string[]|null[] A one-level array of references with Puli paths
     *                         as keys.
     */
    private function flattenWithFilter(array $references, $regex, $flags = 0, $maxDepth = 0)
    {
        $result = array();

        foreach ($references as $currentPath => $currentReferences) {
            // Check whether the current entry matches the pattern
            if (!isset($result[$currentPath]) && preg_match($regex, $currentPath)) {
                // If yes, the first stored reference is returned
                $result[$currentPath] = reset($currentReferences);

                if ($flags & self::STOP_ON_FIRST) {
                    return $result;
                }
            }

            if ($flags & self::NO_SEARCH_FILESYSTEM) {
                continue;
            }

            // First follow any links before we check which of them is a directory
            $currentReferences = $this->followLinks($currentReferences);
            $currentPath = rtrim($currentPath, '/');

            // Search the nested entries if desired
            foreach ($currentReferences as $baseFilesystemPath) {
                // Ignore null values and file paths
                if (!is_dir($baseFilesystemPath)) {
                    continue;
                }

                $iterator = new RecursiveIteratorIterator(
                    new RecursiveDirectoryIterator(
                        $baseFilesystemPath,
                        RecursiveDirectoryIterator::CURRENT_AS_PATHNAME
                            | RecursiveDirectoryIterator::SKIP_DOTS
                    ),
                    RecursiveIteratorIterator::SELF_FIRST
                );

                if (0 !== $maxDepth) {
                    $currentDepth = $this->getPathDepth($currentPath);
                    $maxIteratorDepth = $maxDepth - $currentDepth;

                    if ($maxIteratorDepth < 1) {
                        continue;
                    }

                    $iterator->setMaxDepth($maxIteratorDepth);
                }

                $basePathLength = strlen($baseFilesystemPath);

                foreach ($iterator as $nestedFilesystemPath) {
                    $nestedPath = substr_replace($nestedFilesystemPath, $currentPath, 0, $basePathLength);

                    if (!isset($result[$nestedPath]) && preg_match($regex, $nestedPath)) {
                        $result[$nestedPath] = $nestedFilesystemPath;

                        if ($flags & self::STOP_ON_FIRST) {
                            return $result;
                        }
                    }
                }
            }
        }

        return $result;
    }

    /**
     * Filters the JSON file for all references relevant to a given search path.
     *
     * The JSON is scanned starting with the longest mapped Puli path.
     *
     * If the search path is "/a/b", the result includes:
     *
     *  * The references of the mapped path "/a/b".
     *
     * If the flag `INCLUDE_ANCESTORS` is used, the result additionally
     * includes:
     *
     *  * The references of any mapped super path "/a" with the sub-path "/b"
     *    appended.
     *
     * If the flag `INCLUDE_NESTED` is used, the result additionally
     * includes:
     *
     *  * The references of any mapped sub path "/a/b/c".
     *
     * This is useful if you want to look for the children of "/a/b" or scan
     * all descendants for paths matching a given pattern.
     *
     * The result of this method is an array with two levels:
     *
     *  * The first level has Puli paths as keys.
     *  * The second level contains all references for that path, where the
     *    first reference has the highest, the last reference the lowest
     *    priority. The keys of the second level are integers. There may be
     *    holes between any two keys.
     *
     * The references of the second level contain:
     *
     *  * `null` values for virtual resources
     *  * strings starting with "@" for links
     *  * absolute filesystem paths for filesystem resources
     *
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
     *
     * The flag `NO_SEARCH_FILESYSTEM` may be used to check for whether the found
     * paths actually exist on the filesystem.
     *
     * @param string $searchPath The path to search.
     * @param int    $flags      A bitwise combination of the flag constants in
     *                           this class.
     *
     * @return array An array with two levels.
     */
    private function searchReferences($searchPath, $flags = 0)
    {
        $result = array();
        $foundMatchingMappings = false;
        $searchPath = rtrim($searchPath, '/');
        $searchPathForTest = $searchPath.'/';

        foreach ($this->json as $currentPath => $currentReferences) {
            $currentPathForTest = rtrim($currentPath, '/').'/';

            // We found a mapping that matches the search path
            // e.g. mapping /a/b for path /a/b
            if ($searchPathForTest === $currentPathForTest) {
                $foundMatchingMappings = true;
                $currentReferences = $this->resolveReferences($currentPath, $currentReferences, $flags);

                if (empty($currentReferences)) {
                    continue;
                }

                $result[$currentPath] = $currentReferences;

                // Return unless an explicit mapping order is defined
                // In that case, the ancestors need to be searched as well
                if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) {
                    return $result;
                }

                continue;
            }

            // We found a mapping that lies within the search path
            // e.g. mapping /a/b/c for path /a/b
            if (($flags & self::INCLUDE_NESTED) && 0 === strpos($currentPathForTest, $searchPathForTest)) {
                $foundMatchingMappings = true;
                $currentReferences = $this->resolveReferences($currentPath, $currentReferences, $flags);

                if (empty($currentReferences)) {
                    continue;
                }

                $result[$currentPath] = $currentReferences;

                // Return unless an explicit mapping order is defined
                // In that case, the ancestors need to be searched as well
                if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) {
                    return $result;
                }

                continue;
            }

            // We found a mapping that is an ancestor of the search path
            // e.g. mapping /a for path /a/b
            if (0 === strpos($searchPathForTest, $currentPathForTest)) {
                $foundMatchingMappings = true;

                if ($flags & self::INCLUDE_ANCESTORS) {
                    // Include the references of the ancestor
                    $currentReferences = $this->resolveReferences($currentPath, $currentReferences, $flags);

                    if (empty($currentReferences)) {
                        continue;
                    }

                    $result[$currentPath] = $currentReferences;

                    // Return unless an explicit mapping order is defined
                    // In that case, the ancestors need to be searched as well
                    if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) {
                        return $result;
                    }

                    continue;
                }

                if ($flags & self::NO_SEARCH_FILESYSTEM) {
                    continue;
                }

                // Check the filesystem directories pointed to by the ancestors
                // for the searched path
                $nestedPath = substr($searchPath, strlen($currentPathForTest));
                $currentPathWithNested = rtrim($currentPath, '/').'/'.$nestedPath;

                // Follow links so that we can check the nested directories in
                // the final transitive link targets
                $currentReferencesResolved = $this->followLinks(
                    // Never stop on first, since appendNestedPath() might
                    // discard the first but accept the second entry
                    $this->resolveReferences($currentPath, $currentReferences, $flags & (~self::STOP_ON_FIRST))
                );

                // Append the path and check which of the resulting paths exist
                $nestedReferences = $this->appendPathAndFilterExisting(
                    $currentReferencesResolved,
                    $nestedPath,
                    $flags
                );

                // None of the results exists
                if (empty($nestedReferences)) {
                    continue;
                }

                // Return unless an explicit mapping order is defined
                // In that case, the ancestors need to be searched as well
                if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPathWithNested])) {
                    // The nested references already have size 1
                    return array($currentPathWithNested => $nestedReferences);
                }

                // We are traversing long keys before short keys
                // It could be that this entry already exists.
                if (!isset($result[$currentPathWithNested])) {
                    $result[$currentPathWithNested] = $nestedReferences;

                    continue;
                }

                // If no explicit mapping order is defined, simply append the
                // new references to the existing ones
                if (!isset($this->json['_order'][$currentPathWithNested])) {
                    $result[$currentPathWithNested] = array_merge(
                        $result[$currentPathWithNested],
                        $nestedReferences
                    );

                    continue;
                }

                // If an explicit mapping order is defined, store the paths
                // of the mappings that generated each reference set and
                // resolve the order later on
                if (!isset($result[$currentPathWithNested][$currentPathWithNested])) {
                    $result[$currentPathWithNested] = array(
                        $currentPathWithNested => $result[$currentPathWithNested],
                    );
                }

                // Add the new references generated by the current mapping
                $result[$currentPathWithNested][$currentPath] = $nestedReferences;

                continue;
            }

            // We did not find anything but previously found mappings
            // The mappings are sorted alphabetically, so we can safely abort
            if ($foundMatchingMappings) {
                break;
            }
        }

        // Resolve the order where it is explicitly set
        if (!isset($this->json['_order'])) {
            return $result;
        }

        foreach ($result as $currentPath => $referencesByMappedPath) {
            // If no order is defined for the path or if only one mapped path
            // generated references, there's nothing to do
            if (!isset($this->json['_order'][$currentPath]) || !isset($referencesByMappedPath[$currentPath])) {
                continue;
            }

            $orderedReferences = array();

            foreach ($this->json['_order'][$currentPath] as $orderEntry) {
                if (!isset($referencesByMappedPath[$orderEntry['path']])) {
                    continue;
                }

                for ($i = 0; $i < $orderEntry['references'] && count($referencesByMappedPath[$orderEntry['path']]) > 0; ++$i) {
                    $orderedReferences[] = array_shift($referencesByMappedPath[$orderEntry['path']]);
                }

                // Only include references of the first mapped path
                // Since $stopOnFirst is set, those references have a
                // maximum size of 1
                if ($flags & self::STOP_ON_FIRST) {
                    break;
                }
            }

            $result[$currentPath] = $orderedReferences;
        }

        return $result;
    }

    /**
     * Follows any link in a list of references.
     *
     * This method takes all the given references, checks for links starting
     * with "@" and recursively expands those links to their target references.
     * The target references may be `null` or absolute filesystem paths.
     *
     * Null values are returned unchanged.
     *
     * Absolute filesystem paths are returned unchanged.
     *
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
     *
     * @param string[]|null[] $references The references.
     * @param int             $flags      A bitwise combination of the flag
     *                                    constants in this class.
     *
     * @return string[]|null[] The references with all links replaced by their
     *                         target references. If any link pointed to more
     *                         than one target reference, the returned array
     *                         is larger than the passed array (unless the
     *                         argument `$stopOnFirst` was set to `true`).
     */
    private function followLinks(array $references, $flags = 0)
    {
        $result = array();

        foreach ($references as $key => $reference) {
            // Not a link
            if (!$this->isLinkReference($reference)) {
                $result[] = $reference;

                if ($flags & self::STOP_ON_FIRST) {
                    return $result;
                }

                continue;
            }

            $referencedPath = substr($reference, 1);

            // Get all the file system paths that this link points to
            // and append them to the result
            foreach ($this->searchReferences($referencedPath, $flags) as $referencedReferences) {
                // Follow links recursively
                $referencedReferences = $this->followLinks($referencedReferences);

                // Append all resulting target paths to the result
                foreach ($referencedReferences as $referencedReference) {
                    $result[] = $referencedReference;

                    if ($flags & self::STOP_ON_FIRST) {
                        return $result;
                    }
                }
            }
        }

        return $result;
    }

    /**
     * Appends nested paths to references and filters out the existing ones.
     *
     * This method takes all the given references, appends the nested path to
     * each of them and then filters out the results that actually exist on the
     * filesystem.
     *
     * Null references are filtered out.
     *
     * Link references should be followed with {@link followLinks()} before
     * calling this method.
     *
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
     *
     * @param string[]|null[] $references The references.
     * @param string          $nestedPath The nested path to append without
     *                                    leading slash ("/").
     * @param int             $flags      A bitwise combination of the flag
     *                                    constants in this class.
     *
     * @return string[] The references with the nested path appended. Each
     *                  reference is guaranteed to exist on the filesystem.
     */
    private function appendPathAndFilterExisting(array $references, $nestedPath, $flags = 0)
    {
        $result = array();

        foreach ($references as $reference) {
            // Filter out null values
            // Links should be followed before calling this method
            if (null === $reference) {
                continue;
            }

            $nestedReference = rtrim($reference, '/').'/'.$nestedPath;

            if (file_exists($nestedReference)) {
                $result[] = $nestedReference;

                if ($flags & self::STOP_ON_FIRST) {
                    return $result;
                }
            }
        }

        return $result;
    }

    /**
     * Resolves a list of references stored in the JSON.
     *
     * Each reference passed in can be:
     *
     *  * `null`
     *  * a link starting with `@`
     *  * a filesystem path relative to the base directory
     *  * an absolute filesystem path
     *
     * Each reference returned by this method can be:
     *
     *  * `null`
     *  * a link starting with `@`
     *  * an absolute filesystem path
     *
     * Additionally, the results are guaranteed to be an array.
     *
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
     * In that case, the results array has a maximum size of 1.
     *
     * The flag `NO_SEARCH_FILESYSTEM` may be used to check for whether the found
     * paths actually exist on the filesystem.
     *
     * @param string $path       The mapped Puli path.
     * @param mixed  $references The reference(s).
     * @param int    $flags      A bitwise combination of the flag constants in
     *                           this class.
     *
     * @return string[]|null[] The resolved references.
     */
    private function resolveReferences($path, $references, $flags = 0)
    {
        $result = array();

        if (!is_array($references)) {
            $references = array($references);
        }

        foreach ($references as $key => $reference) {
            // Keep non-filesystem references as they are
            if (!$this->isFilesystemReference($reference)) {
                $result[] = $reference;

                if ($flags & self::STOP_ON_FIRST) {
                    return $result;
                }

                continue;
            }

            $absoluteReference = Path::makeAbsolute($reference, $this->baseDirectory);
            $referenceExists = file_exists($absoluteReference);

            if (($flags & self::NO_CHECK_FILE_EXISTS) || $referenceExists) {
                $result[] = $absoluteReference;

                if ($flags & self::STOP_ON_FIRST) {
                    return $result;
                }
            }

            if (!$referenceExists) {
                $this->logReferenceNotFound($path, $reference, $absoluteReference);
            }
        }

        return $result;
    }

    /**
     * Returns the depth of a Puli path.
     *
     * The depth is used in order to limit the recursion when recursively
     * iterating directories.
     *
     * The depth starts at 0 for the root:
     *
     * /                0
     * /webmozart       1
     * /webmozart/puli  2
     * ...
     *
     * @param string $path A Puli path.
     *
     * @return int The depth starting with 0 for the root node.
     */
    private function getPathDepth($path)
    {
        // / has depth 0
        // /webmozart has depth 1
        // /webmozart/puli has depth 2
        // ...
        return substr_count(rtrim($path, '/'), '/');
    }

    /**
     * Inserts a path at the beginning of the order list of a mapped path.
     *
     * @param string $path          The path of the mapping where to prepend.
     * @param string $prependedPath The path of the mapping to prepend.
     */
    private function prependOrderEntry($path, $prependedPath)
    {
        $lastEntry = reset($this->json['_order'][$path]);

        if ($prependedPath === $lastEntry['path']) {
            // If the first entry matches the new one, add the reference
            // of the current resource to the limit
            ++$lastEntry['references'];
        } else {
            array_unshift($this->json['_order'][$path], array(
                'path' => $prependedPath,
                'references' => 1,
            ));
        }
    }

    /**
     * Initializes a path with the order of the closest parent path.
     *
     * @param string $path             The path to initialize.
     * @param array  $parentReferences The defined references for parent paths,
     *                                 with long paths /a/b sorted before short
     *                                 paths /a.
     */
    private function initWithParentOrder($path, array $parentReferences)
    {
        foreach ($parentReferences as $parentPath => $_) {
            // Look for the first parent entry for which an order is defined
            if (isset($this->json['_order'][$parentPath])) {
                // Inherit that order
                $this->json['_order'][$path] = $this->json['_order'][$parentPath];

                return;
            }
        }
    }

    /**
     * Initializes the order of a path with the default order.
     *
     * This is necessary if we want to insert a non-default order entry for
     * the first time.
     *
     * @param string $path         The path to initialize.
     * @param string $insertedPath The path that is being inserted.
     * @param array  $references   The references for each defined path mapping
     *                             in the path chain.
     */
    private function initWithDefaultOrder($path, $insertedPath, $references)
    {
        $this->json['_order'][$path] = array();

        // Insert the default order, if none exists
        // i.e. long paths /a/b/c before short paths /a/b
        $parentPath = $path;

        while (true) {
            if (isset($references[$parentPath])) {
                $parentEntry = array(
                    'path' => $parentPath,
                    'references' => count($references[$parentPath]),
                );

                // Edge case: $parentPath equals $insertedPath. In this case we have
                // to subtract the entry that we're adding
                if ($parentPath === $insertedPath) {
                    --$parentEntry['references'];
                }

                if (0 !== $parentEntry['references']) {
                    $this->json['_order'][$path][] = $parentEntry;
                }
            }

            if ('/' === $parentPath) {
                break;
            }

            $parentPath = Path::getDirectory($parentPath);
        }
    }
}


================================================
FILE: src/NullRepository.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository;

use Puli\Repository\Api\EditableRepository;
use Puli\Repository\Api\NoVersionFoundException;
use Puli\Repository\Api\ResourceNotFoundException;
use Puli\Repository\Resource\Collection\ArrayResourceCollection;

/**
 * A repository that does nothing.
 *
 * This repository can be used if you need to inject a repository instance in
 * some code, but you don't want that repository to do anything (for example
 * in tests).
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class NullRepository implements EditableRepository
{
    /**
     * {@inheritdoc}
     */
    public function add($path, $resource)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function remove($query, $language = 'glob')
    {
    }

    /**
     * {@inheritdoc}
     */
    public function clear()
    {
    }

    /**
     * {@inheritdoc}
     */
    public function get($path)
    {
        throw ResourceNotFoundException::forPath($path);
    }

    /**
     * {@inheritdoc}
     */
    public function getVersions($path)
    {
        throw NoVersionFoundException::forPath($path);
    }

    /**
     * {@inheritdoc}
     */
    public function find($query, $language = 'glob')
    {
        return new ArrayResourceCollection();
    }

    /**
     * {@inheritdoc}
     */
    public function contains($query, $language = 'glob')
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function hasChildren($path)
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function listChildren($path)
    {
        return new ArrayResourceCollection();
    }
}


================================================
FILE: src/OptimizedJsonRepository.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository;

use Puli\Repository\Api\EditableRepository;
use Puli\Repository\Api\Resource\FilesystemResource;
use Webmozart\Glob\Glob;
use Webmozart\PathUtil\Path;

/**
 * A repository backed by a JSON file optimized for reading.
 *
 * The generated JSON file is described by res/schema/repository-schema-1.0.json.
 *
 * Resources can be added with the method {@link add()}:
 *
 * ```php
 * use Puli\Repository\OptimizedJsonRepository;
 *
 * $repo = new OptimizedJsonRepository('/path/to/repository.json', '/path/to/project');
 * $repo->add('/css', new DirectoryResource('/path/to/project/res/css'));
 * ```
 *
 * When adding a resource, the added filesystem path is stored in the JSON file
 * under the key of the Puli path. The path is stored relatively to the base
 * directory passed to the constructor. Directories will be expanded and all
 * nested files will be added to the mapping file as well:
 *
 * ```json
 * {
 *     "/css": "res/css",
 *     "/css/style.css": "res/css/style.css"
 * }
 * ```
 *
 * Mapped resources can be read with the method {@link get()}:
 *
 * ```php
 * $cssPath = $repo->get('/css')->getFilesystemPath();
 * ```
 *
 * You can also access nested files:
 *
 * ```php
 * echo $repo->get('/css/style.css')->getBody();
 * ```
 *
 * Since nested files are searched during {@link add()} and added to the JSON
 * file, this repository does not detect any files that you add to a directory
 * after adding that directory to the repository. This means that accessing
 * files is very fast, but also that the usage of this repository implementation
 * can be cumbersome in development environments. There you are recommended to
 * use {@link JsonRepository} instead.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 * @author Titouan Galopin <galopintitouan@gmail.com>
 */
class OptimizedJsonRepository extends AbstractJsonRepository implements EditableRepository
{
    /**
     * {@inheritdoc}
     */
    public function clear()
    {
        if (null === $this->json) {
            $this->load();
        }

        // Subtract root which is not deleted
        $removed = count($this->json) - 1;

        $this->json = array();

        $this->flush();

        $this->clearVersions();
        $this->storeVersion($this->get('/'));

        return $removed;
    }

    /**
     * {@inheritdoc}
     */
    protected function insertReference($path, $reference)
    {
        $this->json[$path] = $reference;
    }

    /**
     * {@inheritdoc}
     */
    protected function removeReferences($glob)
    {
        $removed = 0;

        foreach ($this->getReferencesForGlob($glob.'{,/**/*}') as $path => $reference) {
            ++$removed;

            $this->removeVersions($path);

            unset($this->json[$path]);
        }

        return $removed;
    }

    /**
     * {@inheritdoc}
     */
    protected function getReferencesForPath($path)
    {
        if (!array_key_exists($path, $this->json)) {
            return array();
        }

        $reference = $this->json[$path];

        // We're only interested in the first entry of eventual arrays
        if (is_array($reference)) {
            $reference = reset($reference);
        }

        if ($this->isFilesystemReference($reference)) {
            $absoluteReference = Path::makeAbsolute($reference, $this->baseDirectory);

            if (!file_exists($absoluteReference)) {
                $this->logReferenceNotFound($path, $reference, $absoluteReference);

                return array();
            }

            $reference = $absoluteReference;
        }

        return array($path => $reference);
    }

    /**
     * {@inheritdoc}
     */
    protected function getReferencesForGlob($glob, $flags = 0)
    {
        if (!Glob::isDynamic($glob)) {
            return $this->getReferencesForPath($glob);
        }

        return $this->getReferencesForRegex(
            Glob::getStaticPrefix($glob),
            Glob::toRegEx($glob),
            $flags
        );
    }

    /**
     * {@inheritdoc}
     */
    protected function getReferencesForRegex($staticPrefix, $regex, $flags = 0)
    {
        $result = array();
        $foundMappingsWithPrefix = false;

        foreach ($this->json as $path => $reference) {
            if (0 === strpos($path, $staticPrefix)) {
                $foundMappingsWithPrefix = true;

                if (!preg_match($regex, $path)) {
                    continue;
                }

                // We're only interested in the first entry of eventual arrays
                if (is_array($reference)) {
                    $reference = reset($reference);
                }

                if ($this->isFilesystemReference($reference)) {
                    $absoluteReference = Path::makeAbsolute($reference, $this->baseDirectory);

                    if (!file_exists($absoluteReference)) {
                        $this->logReferenceNotFound($path, $reference, $absoluteReference);

                        continue;
                    }

                    $reference = $absoluteReference;
                }

                $result[$path] = $reference;

                if ($flags & self::STOP_ON_FIRST) {
                    return $result;
                }

                continue;
            }

            // We did not find anything but previously found mappings with the
            // static prefix
            // The mappings are sorted alphabetically, so we can safely abort
            if ($foundMappingsWithPrefix) {
                break;
            }
        }

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    protected function getReferencesInDirectory($path, $flags = 0)
    {
        $basePath = rtrim($path, '/');

        return $this->getReferencesForRegex(
            $basePath.'/',
            '~^'.preg_quote($basePath, '~').'/[^/]+$~',
            $flags
        );
    }

    /**
     * {@inheritdoc}
     */
    protected function addFilesystemResource($path, FilesystemResource $resource)
    {
        // Read children before attaching the resource to this repository
        $children = $resource->listChildren();

        parent::addFilesystemResource($path, $resource);

        // Recursively add all child resources
        $basePath = '/' === $path ? $path : $path.'/';

        foreach ($children as $name => $child) {
            $this->addFilesystemResource($basePath.$name, $child);
        }
    }
}


================================================
FILE: src/RepositoryFactoryException.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository;

use RuntimeException;

/**
 * Thrown if a factory closure did not create valid repository.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class RepositoryFactoryException extends RuntimeException
{
}


================================================
FILE: src/Resource/AbstractFilesystemResource.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Resource;

use Puli\Repository\Api\Resource\FilesystemResource;
use Puli\Repository\Resource\Metadata\FilesystemMetadata;

/**
 * Base class for filesystem resources.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
abstract class AbstractFilesystemResource extends GenericResource implements FilesystemResource
{
    /**
     * @var string
     */
    private $filesystemPath;

    /**
     * Creates a new filesystem resource.
     *
     * @param string      $filesystemPath The path on the file system.
     * @param string|null $path           The repository path of the resource.
     */
    public function __construct($filesystemPath, $path = null)
    {
        parent::__construct($path);

        $this->filesystemPath = str_replace(DIRECTORY_SEPARATOR, '/', $filesystemPath);
    }

    /**
     * {@inheritdoc}
     */
    public function getFilesystemPath()
    {
        return $this->filesystemPath;
    }

    /**
     * {@inheritdoc}
     */
    public function getMetadata()
    {
        return new FilesystemMetadata($this->filesystemPath);
    }

    protected function preSerialize(array &$data)
    {
        parent::preSerialize($data);

        $data[] = $this->filesystemPath;
    }

    protected function postUnserialize(array $data)
    {
        $this->filesystemPath = array_pop($data);

        parent::postUnserialize($data);
    }
}


================================================
FILE: src/Resource/Collection/ArrayResourceCollection.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Resource\Collection;

use InvalidArgumentException;
use IteratorAggregate;
use OutOfBoundsException;
use Puli\Repository\Api\Resource\PuliResource;
use Puli\Repository\Api\ResourceCollection;
use Puli\Repository\Api\UnsupportedResourceException;
use Puli\Repository\Resource\Iterator\ResourceCollectionIterator;
use Traversable;
use Webmozart\Assert\Assert;

/**
 * A collection of {@link PuliResource} instances backed by an array.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class ArrayResourceCollection implements IteratorAggregate, ResourceCollection
{
    /**
     * @var PuliResource[]|Traversable
     */
    private $resources;

    /**
     * Creates a new collection.
     *
     * You can pass the resources that you want to initially store in the
     * collection as argument.
     *
     * @param PuliResource[]|Traversable $resources The resources to store in the collection.
     *
     * @throws InvalidArgumentException     If the resources are not an array
     *                                      and not a traversable object.
     * @throws UnsupportedResourceException If a resource does not implement
     *                                      {@link PuliResource}.
     */
    public function __construct($resources = array())
    {
        $this->replace($resources);
    }

    /**
     * {@inheritdoc}
     */
    public function add(PuliResource $resource)
    {
        $this->resources[] = $resource;
    }

    /**
     * {@inheritdoc}
     */
    public function set($key, PuliResource $resource)
    {
        $this->resources[$key] = $resource;
    }

    /**
     * {@inheritdoc}
     */
    public function get($key)
    {
        if (!isset($this->resources[$key])) {
            throw new OutOfBoundsException(sprintf(
                'The offset "%s" does not exist.',
                $key
            ));
        }

        return $this->resources[$key];
    }

    /**
     * {@inheritdoc}
     */
    public function remove($key)
    {
        unset($this->resources[$key]);
    }

    /**
     * {@inheritdoc}
     */
    public function has($key)
    {
        return isset($this->resources[$key]);
    }

    /**
     * {@inheritdoc}
     */
    public function clear()
    {
        $this->resources = array();
    }

    /**
     * {@inheritdoc}
     */
    public function keys()
    {
        return array_keys($this->resources);
    }

    /**
     * {@inheritdoc}
     */
    public function replace($resources)
    {
        Assert::allIsInstanceOf($resources, 'Puli\Repository\Api\Resource\PuliResource');

        $this->resources = is_array($resources) ? $resources : iterator_to_array($resources);
    }

    /**
     * {@inheritdoc}
     */
    public function merge($resources)
    {
        Assert::allIsInstanceOf($resources, 'Puli\Repository\Api\Resource\PuliResource');

        // only start merging after validating all resources
        foreach ($resources as $resource) {
            $this->resources[] = $resource;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function isEmpty()
    {
        return 0 === count($this->resources);
    }

    /**
     * {@inheritdoc}
     */
    public function offsetExists($key)
    {
        return $this->has($key);
    }

    /**
     * {@inheritdoc}
     */
    public function offsetGet($key)
    {
        return $this->get($key);
    }

    /**
     * {@inheritdoc}
     */
    public function offsetSet($key, $value)
    {
        if (null !== $key) {
            $this->set($key, $value);
        } else {
            $this->add($value);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function offsetUnset($key)
    {
        $this->remove($key);
    }

    /**
     * {@inheritdoc}
     */
    public function getPaths()
    {
        return array_map(
            function (PuliResource $resource) {
                return $resource->getPath();
            },
            $this->resources
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getNames()
    {
        return array_map(
            function (PuliResource $resource) {
                return $resource->getName();
            },
            $this->resources
        );
    }

    public function count()
    {
        return count($this->resources);
    }

    public function getIterator($mode = ResourceCollectionIterator::KEY_AS_CURSOR)
    {
        return new ResourceCollectionIterator($this, $mode);
    }

    public function toArray()
    {
        return $this->resources;
    }
}


================================================
FILE: src/Resource/Collection/FilesystemResourceCollection.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Resource\Collection;

use Puli\Repository\Api\Resource\FilesystemResource;
use Puli\Repository\Api\Resource\PuliResource;

/**
 * A collection of resources on the filesystem.
 *
 * The resource collection contains the additional method
 * {@link getFilesystemPaths()} for batch collecting the filesystem paths of all
 * contained resources.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class FilesystemResourceCollection extends ArrayResourceCollection
{
    /**
     * Returns the filesystem paths of all contained resources.
     *
     * The paths are returned in order of the resources. Resources that are not
     * on the filesystem are ignored and not contained in the output.
     *
     * @return string[] The filesystem paths.
     */
    public function getFilesystemPaths()
    {
        return array_map(
            function (FilesystemResource $resource) {
                return $resource->getFilesystemPath();
            },
            array_filter(
                $this->toArray(),
                function (PuliResource $r) {
                    return $r instanceof FilesystemResource && null !== $r->getFilesystemPath();
                }
            )
        );
    }
}


================================================
FILE: src/Resource/Collection/LazyResourceCollection.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Resource\Collection;

use BadMethodCallException;
use IteratorAggregate;
use OutOfBoundsException;
use Puli\Repository\Api\Resource\PuliResource;
use Puli\Repository\Api\ResourceCollection;
use Puli\Repository\Api\ResourceRepository;
use Puli\Repository\Resource\Iterator\ResourceCollectionIterator;
use Traversable;

/**
 * A resource collection which loads its resources on demand.
 *
 * This collection is read-only. Each resource is loaded when it is accessed
 * for the first time.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class LazyResourceCollection implements IteratorAggregate, ResourceCollection
{
    /**
     * @var string[]|PuliResource[]
     */
    private $resources;

    /**
     * @var ResourceRepository
     */
    private $repo;

    /**
     * @var bool
     */
    private $loaded = false;

    /**
     * Creates a new collection.
     *
     * @param ResourceRepository $repo  The repository that will be used to load
     *                                  the resources.
     * @param array              $paths The paths of the resources which will be
     *                                  loaded into the collection.
     */
    public function __construct(ResourceRepository $repo, array $paths)
    {
        $this->resources = $paths;
        $this->repo = $repo;
    }

    /**
     * Not supported.
     *
     * @param PuliResource $resource The resource to add.
     *
     * @throws BadMethodCallException The collection is read-only.
     */
    public function add(PuliResource $resource)
    {
        throw new BadMethodCallException(
            'Lazy resource collections cannot be modified.'
        );
    }

    /**
     * Not supported.
     *
     * @param int          $key      The collection key.
     * @param PuliResource $resource The resource to add.
     *
     * @throws BadMethodCallException The collection is read-only.
     */
    public function set($key, PuliResource $resource)
    {
        throw new BadMethodCallException(
            'Lazy resource collections cannot be modified.'
        );
    }

    /**
     * {@inheritdoc}
     */
    public function get($key)
    {
        if (!isset($this->resources[$key])) {
            throw new OutOfBoundsException(sprintf(
                'The offset "%s" does not exist.',
                $key
            ));
        }

        if (!$this->resources[$key] instanceof PuliResource) {
            $this->resources[$key] = $this->repo->get($this->resources[$key]);
        }

        return $this->resources[$key];
    }

    /**
     * Not supported.
     *
     * @param string $key The collection key to remove.
     *
     * @throws BadMethodCallException The collection is read-only.
     */
    public function remove($key)
    {
        throw new BadMethodCallException(
            'Lazy resource collections cannot be modified.'
        );
    }

    /**
     * {@inheritdoc}
     */
    public function has($key)
    {
        return isset($this->resources[$key]);
    }

    /**
     * Not supported.
     *
     * @throws BadMethodCallException The collection is read-only.
     */
    public function clear()
    {
        throw new BadMethodCallException(
            'Lazy resource collections cannot be modified.'
        );
    }

    /**
     * {@inheritdoc}
     */
    public function keys()
    {
        if (!$this->loaded) {
            $this->load();
        }

        return array_keys($this->resources);
    }

    /**
     * Not supported.
     *
     * @param PuliResource[]|Traversable $resources The resources to replace the
     *                                              collection contents with.
     *
     * @throws BadMethodCallException The collection is read-only.
     */
    public function replace($resources)
    {
        throw new BadMethodCallException(
            'Lazy resource collections cannot be modified.'
        );
    }

    /**
     * Not supported.
     *
     * @param PuliResource[]|Traversable $resources The resources to merge into
     *                                              the collection.
     *
     * @throws BadMethodCallException The collection is read-only.
     */
    public function merge($resources)
    {
        throw new BadMethodCallException(
            'Lazy resource collections cannot be modified.'
        );
    }

    /**
     * {@inheritdoc}
     */
    public function isEmpty()
    {
        return 0 === count($this->resources);
    }

    /**
     * {@inheritdoc}
     */
    public function offsetExists($key)
    {
        return $this->has($key);
    }

    /**
     * {@inheritdoc}
     */
    public function offsetGet($key)
    {
        return $this->get($key);
    }

    /**
     * Not supported.
     *
     * @param string       $key      The collection key to set.
     * @param PuliResource $resource The resource to set.
     *
     * @throws BadMethodCallException The collection is read-only.
     */
    public function offsetSet($key, $resource)
    {
        throw new BadMethodCallException(
            'Lazy resource collections cannot be modified.'
        );
    }

    /**
     * Not supported.
     *
     * @param string $key The collection key to remove.
     *
     * @throws BadMethodCallException The collection is read-only.
     */
    public function offsetUnset($key)
    {
        throw new BadMethodCallException(
            'Lazy resource collections cannot be modified.'
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getPaths()
    {
        if (!$this->loaded) {
            $this->load();
        }

        return array_map(
            function (PuliResource $resource) {
                return $resource->getPath();
            },
            $this->resources
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getNames()
    {
        if (!$this->loaded) {
            $this->load();
        }

        return array_map(
            function (PuliResource $resource) {
                return $resource->getName();
            },
            $this->resources
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getIterator($mode = ResourceCollectionIterator::KEY_AS_CURSOR)
    {
        if (!$this->loaded) {
            $this->load();
        }

        return new ResourceCollectionIterator($this, $mode);
    }

    /**
     * {@inheritdoc}
     */
    public function toArray()
    {
        if (!$this->loaded) {
            $this->load();
        }

        return $this->resources;
    }

    /**
     * {@inheritdoc}
     */
    public function count()
    {
        return count($this->resources);
    }

    /**
     * Loads the complete collection.
     */
    private function load()
    {
        foreach ($this->resources as $key => $resource) {
            if (!$resource instanceof PuliResource) {
                $this->resources[$key] = $this->repo->get($resource);
            }
        }

        $this->loaded = true;
    }
}


================================================
FILE: src/Resource/DirectoryResource.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Resource;

use Puli\Repository\Api\ResourceNotFoundException;
use Puli\Repository\Resource\Collection\FilesystemResourceCollection;
use Webmozart\Assert\Assert;
use Webmozart\Glob\Iterator\RecursiveDirectoryIterator;

/**
 * Represents a directory on the file system.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class DirectoryResource extends AbstractFilesystemResource
{
    /**
     * {@inheritdoc}
     */
    public function __construct($filesystemPath, $path = null)
    {
        Assert::directory($filesystemPath);

        parent::__construct($filesystemPath, $path);
    }

    /**
     * {@inheritdoc}
     */
    public function getChild($relPath)
    {
        // Use attached repository if possible
        if ($this->getRepository()) {
            return $this->getRepository()->get($this->getRepositoryPath().'/'.$relPath);
        }

        $filesystemPath = $this->getFilesystemPath().'/'.$relPath;

        if (!file_exists($filesystemPath)) {
            throw ResourceNotFoundException::forPath($this->getPath().'/'.$relPath);
        }

        $childPath = null === $this->getPath() ? null : $this->getPath().'/'.$relPath;

        return is_dir($filesystemPath)
            ? new self($filesystemPath, $childPath)
            : new FileResource($filesystemPath, $childPath);
    }

    /**
     * {@inheritdoc}
     */
    public function hasChild($relPath)
    {
        // Use attached repository if possible
        if ($this->getRepository()) {
            return $this->getRepository()->contains($this->getRepositoryPath().'/'.$relPath);
        }

        return file_exists($this->getFilesystemPath().'/'.$relPath);
    }

    /**
     * {@inheritdoc}
     */
    public function hasChildren()
    {
        // Use attached repository if possible
        if ($this->getRepository()) {
            return $this->getRepository()->hasChildren($this->getRepositoryPath());
        }

        $iterator = new RecursiveDirectoryIterator(
            $this->getFilesystemPath(),
            RecursiveDirectoryIterator::SKIP_DOTS
        );
        $iterator->rewind();

        return $iterator->valid();
    }

    /**
     * {@inheritdoc}
     */
    public function listChildren()
    {
        $children = new FilesystemResourceCollection();

        // Use attached repository if possible
        if ($this->getRepository()) {
            foreach ($this->getRepository()->listChildren($this->getRepositoryPath()) as $child) {
                $children[$child->getName()] = $child;
            }

            return $children;
        }

        $iterator = new RecursiveDirectoryIterator(
            $this->getFilesystemPath(),
            RecursiveDirectoryIterator::CURRENT_AS_PATHNAME | RecursiveDirectoryIterator::SKIP_DOTS
        );

        // We can't use glob() here, because glob() doesn't list files starting
        // with "." by default
        foreach ($iterator as $path) {
            $children[basename($path)] = is_dir($path)
                ? new self($path)
                : new FileResource($path);
        }

        return $children;
    }
}


================================================
FILE: src/Resource/FileResource.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Resource;

use Puli\Repository\Api\Resource\BodyResource;
use Puli\Repository\Api\ResourceNotFoundException;
use Puli\Repository\Resource\Collection\ArrayResourceCollection;
use Webmozart\Assert\Assert;

/**
 * Represents a file on the file system.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class FileResource extends AbstractFilesystemResource implements BodyResource
{
    /**
     * {@inheritdoc}
     */
    public function __construct($filesystemPath, $path = null)
    {
        Assert::file($filesystemPath);

        parent::__construct($filesystemPath, $path);
    }

    /**
     * {@inheritdoc}
     */
    public function getBody()
    {
        return file_get_contents($this->getFilesystemPath());
    }

    /**
     * {@inheritdoc}
     */
    public function getChild($relPath)
    {
        throw ResourceNotFoundException::forPath($this->getPath().'/'.$relPath);
    }

    /**
     * {@inheritdoc}
     */
    public function hasChild($relPath)
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function hasChildren()
    {
        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function listChildren()
    {
        return new ArrayResourceCollection();
    }
}


================================================
FILE: src/Resource/GenericResource.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Resource;

use Puli\Repository\Api\ChangeStream\VersionList;
use Puli\Repository\Api\Resource\PuliResource;
use Puli\Repository\Api\Resource\ResourceMetadata;
use Puli\Repository\Api\ResourceNotFoundException;
use Puli\Repository\Api\ResourceRepository;
use Puli\Repository\Resource\Collection\ArrayResourceCollection;

/**
 * A generic resource.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class GenericResource implements PuliResource
{
    /**
     * @var ResourceRepository
     */
    private $repo;

    /**
     * @var string
     */
    private $path;

    /**
     * @var string
     */
    private $repoPath;

    /**
     * Creates a new resource.
     *
     * @param string|null $path The path of the resource.
     */
    public function __construct($path = null)
    {
        $this->path = $path;
        $this->repoPath = $path;
    }

    /**
     * {@inheritdoc}
     */
    public function getPath()
    {
        return $this->path;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return $this->path ? basename($this->path) : null;
    }

    /**
     * {@inheritdoc}
     */
    public function getChild($relPath)
    {
        if (!$this->getRepository()) {
            throw ResourceNotFoundException::forPath($this->getRepositoryPath().'/'.$relPath);
        }

        return $this->getRepository()->get($this->getRepositoryPath().'/'.$relPath);
    }

    /**
     * {@inheritdoc}
     */
    public function hasChild($relPath)
    {
        if (!$this->getRepository()) {
            return false;
        }

        return $this->getRepository()->contains($this->getRepositoryPath().'/'.$relPath);
    }

    /**
     * {@inheritdoc}
     */
    public function hasChildren()
    {
        if (!$this->getRepository()) {
            return false;
        }

        return $this->getRepository()->hasChildren($this->getRepositoryPath());
    }

    /**
     * {@inheritdoc}
     */
    public function listChildren()
    {
        $children = new ArrayResourceCollection();

        if (!$this->getRepository()) {
            return $children;
        }

        foreach ($this->getRepository()->listChildren($this->getRepositoryPath()) as $child) {
            $children[$child->getName()] = $child;
        }

        return $children;
    }

    /**
     * {@inheritdoc}
     */
    public function getVersions()
    {
        if (!$this->getRepository()) {
            return new VersionList($this->getRepositoryPath(), array($this));
        }

        return $this->getRepository()->getVersions($this->getRepositoryPath());
    }

    /**
     * {@inheritdoc}
     */
    public function getMetadata()
    {
        return new ResourceMetadata();
    }

    /**
     * {@inheritdoc}
     */
    public function attachTo(ResourceRepository $repo, $path = null)
    {
        $this->repo = $repo;

        if (null !== $path) {
            $this->path = $path;
            $this->repoPath = $path;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function detach()
    {
        $this->repo = null;
    }

    /**
     * {@inheritdoc}
     */
    public function getRepository()
    {
        return $this->repo;
    }

    /**
     * {@inheritdoc}
     */
    public function getRepositoryPath()
    {
        return $this->repoPath;
    }

    /**
     * {@inheritdoc}
     */
    public function isAttached()
    {
        return null !== $this->repo;
    }

    /**
     * {@inheritdoc}
     */
    public function createReference($path)
    {
        $ref = clone $this;
        $ref->path = $path;

        return $ref;
    }

    /**
     * {@inheritdoc}
     */
    public function isReference()
    {
        return $this->path !== $this->repoPath;
    }

    /**
     * {@inheritdoc}
     */
    public function serialize()
    {
        $data = array();

        $this->preSerialize($data);

        return serialize($data);
    }

    /**
     * {@inheritdoc}
     */
    public function unserialize($string)
    {
        $data = unserialize($string);

        $this->postUnserialize($data);
    }

    /**
     * Invoked before serializing a resource.
     *
     * Override this method if you want to serialize custom data in subclasses.
     *
     * @param array $data The data to serialize. Add custom data at the end of
     *                    the array.
     */
    protected function preSerialize(array &$data)
    {
        $data[] = $this->path;
        $data[] = $this->repoPath;
    }

    /**
     * Invoked after unserializing a resource.
     *
     * Override this method if you want to unserialize custom data in
     * subclasses.
     *
     * @param array $data The unserialized data. Pop your custom data from the
     *                    end of the array before calling the parent method.
     */
    protected function postUnserialize(array $data)
    {
        $this->repoPath = array_pop($data);
        $this->path = array_pop($data);
    }
}


================================================
FILE: src/Resource/Iterator/RecursiveResourceIterator.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Resource\Iterator;

use Puli\Repository\Api\ResourceIterator;
use RecursiveIterator;

/**
 * A resource iterator that can be iterated recursively.
 *
 * Use {@link RecursiveResourceIteratorIterator} to iterate over the iterator.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
interface RecursiveResourceIterator extends ResourceIterator, RecursiveIterator
{
}


================================================
FILE: src/Resource/Iterator/RecursiveResourceIteratorIterator.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Resource\Iterator;

use Puli\Repository\Api\ResourceIterator;
use RecursiveIteratorIterator;

/**
 * Iterates recursively over {@link RecursiveResourceIterator} instances.
 *
 * Use this iterator to iterate recursively over a recursive resource iterator:
 *
 * ```php
 * $iterator = new RecursiveResourceIteratorIterator(
 *     new ResourceCollectionIterator(
 *         $collection,
 *         ResourceCollectionIterator::KEY_AS_PATH | ResourceCollectionIterator::CURRENT_AS_RESOURCE
 *     ),
 *     RecursiveResourceIteratorIterator::SELF_FIRST
 * );
 *
 * foreach ($iterator as $path => $resource) {
 *     // ...
 * }
 * ```
 *
 * The configuration of this iterator works identically to its parent class
 * {@link RecursiveIteratorIterator}.
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class RecursiveResourceIteratorIterator extends RecursiveIteratorIterator implements ResourceIterator
{
    /**
     * Creates a new iterator.
     *
     * @param RecursiveResourceIterator $iterator The inner iterator.
     * @param int                       $mode     The iteration mode.
     * @param int                       $flags    The iteration flags.
     *
     * @see RecursiveIteratorIterator::__construct
     */
    public function __construct(RecursiveResourceIterator $iterator, $mode = self::LEAVES_ONLY, $flags = 0)
    {
        parent::__construct($iterator, $mode, $flags);
    }

    /**
     * {@inheritdoc}
     */
    public function getCurrentResource()
    {
        return $this->getInnerIterator()->getCurrentResource();
    }
}


================================================
FILE: src/Resource/Iterator/ResourceCollectionIterator.php
================================================
<?php

/*
 * This file is part of the puli/repository package.
 *
 * (c) Bernhard Schussek <bschussek@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Puli\Repository\Resource\Iterator;

use Puli\Repository\Api\Resource\PuliResource;
use Puli\Repository\Api\ResourceCollection;

/**
 * A recursive iterator for resource collections.
 *
 * Use the iterator if you want to iterate a resource collection. You can
 * configure what the iterator should return as keys and values:
 *
 * ```php
 * $iterator = new ResourceCollectionIterator(
 *     $collection,
 *     ResourceCollectionIterator::KEY_AS_PATH | ResourceCollectionIterator::CURRENT_AS_RESOURCE
 * );
 *
 * foreach ($iterator as $path => $resource) {
 *     // ...
 * }
 * ```
 *
 * If you want to iterate the collection recursively, wrap it in a
 * {@link RecursiveResourceIteratorIterator}:
 *
 * ```php
 * $iterator = new RecursiveResourceIteratorIterator(
 *     new ResourceCollectionIterator(
 *         $collection,
 *         ResourceCollectionIterator::KEY_AS_PATH | ResourceCollectionIterator::CURRENT_AS_RESOURCE
 *     ),
 *     RecursiveResourceIteratorIterator::SELF_FIRST
 * );
 *
 * foreach ($iterator as $path => $resource) {
 *     // ...
 * }
 * ```
 *
 * @since  1.0
 *
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class ResourceCollectionIterator implements RecursiveResourceIterator
{
    /**
     * Return {@link PuliResource} instances as values.
     */
    const CURRENT_AS_RESOURCE = 1;

    /**
     * Return the paths of the resources as values.
     */
    const CURRENT_AS_PATH = 2;

    /**
     * Return the names of the resources as values.
     */
    const CURRENT_AS_NAME = 4;

    /**
     * Return the paths of the resources as keys.
     */
    const KEY_AS_PATH = 64;

    /**
     * Return the collection keys as keys.
     *
     * Attention: Don't use this mode when iterating recursively, as PHP's
     * {@link RecursiveIteratorIterator} skips inner nodes then.
     */
    const KEY_AS_CURSOR = 128;

    /**
     * @var PuliResource[]
     */
    protected $resources;

    /**
     * @var int
     */
    protected $mode;

    /**
     * Creates a new iterator.
     *
     * The following constants can be used to configure the values returned by
     * the iterator:
     *
     *  * {@link CURRENT_AS_RESOURCE}: The {@link PuliResource} objects are
     *                                 returned as values;
     *  * {@link CURRENT_AS_PATH}: The resource paths are returned as values;
     *  * {@link CURRENT_AS_NAME}: The resource names are returned a
Download .txt
gitextract_vpugdgzo/

├── .composer-auth.json
├── .gitattributes
├── .gitignore
├── .styleci.yml
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── appveyor.yml
├── composer.json
├── phpunit.xml.dist
├── res/
│   └── schema/
│       └── path-mappings-schema-1.0.json
├── src/
│   ├── AbstractEditableRepository.php
│   ├── AbstractJsonRepository.php
│   ├── AbstractRepository.php
│   ├── Api/
│   │   ├── ChangeStream/
│   │   │   ├── ChangeStream.php
│   │   │   └── VersionList.php
│   │   ├── EditableRepository.php
│   │   ├── NoVersionFoundException.php
│   │   ├── Resource/
│   │   │   ├── BodyResource.php
│   │   │   ├── FilesystemResource.php
│   │   │   ├── PuliResource.php
│   │   │   └── ResourceMetadata.php
│   │   ├── ResourceCollection.php
│   │   ├── ResourceIterator.php
│   │   ├── ResourceNotFoundException.php
│   │   ├── ResourceRepository.php
│   │   ├── UnsupportedLanguageException.php
│   │   ├── UnsupportedOperationException.php
│   │   └── UnsupportedResourceException.php
│   ├── ChangeStream/
│   │   ├── InMemoryChangeStream.php
│   │   ├── JsonChangeStream.php
│   │   └── KeyValueStoreChangeStream.php
│   ├── Discovery/
│   │   ├── ResourceBinding.php
│   │   └── ResourceBindingInitializer.php
│   ├── FilesystemRepository.php
│   ├── InMemoryRepository.php
│   ├── JsonRepository.php
│   ├── NullRepository.php
│   ├── OptimizedJsonRepository.php
│   ├── RepositoryFactoryException.php
│   ├── Resource/
│   │   ├── AbstractFilesystemResource.php
│   │   ├── Collection/
│   │   │   ├── ArrayResourceCollection.php
│   │   │   ├── FilesystemResourceCollection.php
│   │   │   └── LazyResourceCollection.php
│   │   ├── DirectoryResource.php
│   │   ├── FileResource.php
│   │   ├── GenericResource.php
│   │   ├── Iterator/
│   │   │   ├── RecursiveResourceIterator.php
│   │   │   ├── RecursiveResourceIteratorIterator.php
│   │   │   ├── ResourceCollectionIterator.php
│   │   │   └── ResourceFilterIterator.php
│   │   ├── LinkResource.php
│   │   └── Metadata/
│   │       └── FilesystemMetadata.php
│   ├── StreamWrapper/
│   │   ├── ResourceStreamWrapper.php
│   │   ├── StreamWrapper.php
│   │   └── StreamWrapperException.php
│   └── Uri/
│       ├── InvalidUriException.php
│       └── Uri.php
└── tests/
    ├── AbstractEditableRepositoryTest.php
    ├── AbstractFilesystemRepositorySymlinkTest.php
    ├── AbstractFilesystemRepositoryTest.php
    ├── AbstractJsonRepositoryTest.php
    ├── AbstractRepositoryTest.php
    ├── Api/
    │   └── ChangeStream/
    │       └── VersionListTest.php
    ├── ChangeStream/
    │   ├── AbstractChangeStreamTest.php
    │   ├── InMemoryChangeStreamTest.php
    │   ├── JsonChangeStreamLoadedTest.php
    │   ├── JsonChangeStreamTest.php
    │   └── KeyValueStoreChangeStreamTest.php
    ├── Discovery/
    │   ├── Fixtures/
    │   │   └── SubResourceBinding.php
    │   ├── ResourceBindingInitializerTest.php
    │   └── ResourceBindingTest.php
    ├── FilesystemRepositoryAbsoluteSymlinkTest.php
    ├── FilesystemRepositoryCopyTest.php
    ├── FilesystemRepositoryLoadedTest.php
    ├── FilesystemRepositoryRelativeSymlinkTest.php
    ├── Fixtures/
    │   ├── dir1/
    │   │   ├── file1
    │   │   └── file2
    │   ├── dir2/
    │   │   ├── file2
    │   │   └── file3
    │   ├── dir3/
    │   │   └── sub/
    │   │       ├── file1
    │   │       └── file2
    │   ├── dir4/
    │   │   └── sub/
    │   │       ├── file2
    │   │       └── file3
    │   └── dir5/
    │       ├── file1
    │       ├── file2
    │       └── sub/
    │           ├── file3
    │           └── file4
    ├── InMemoryRepositoryTest.php
    ├── JsonRepositoryLoadedTest.php
    ├── JsonRepositoryTest.php
    ├── NullRepositoryTest.php
    ├── OptimizedJsonRepositoryLoadedTest.php
    ├── OptimizedJsonRepositoryTest.php
    ├── Resource/
    │   ├── AbstractFilesystemResourceTest.php
    │   ├── AbstractResourceTest.php
    │   ├── Collection/
    │   │   ├── ArrayResourceCollectionTest.php
    │   │   └── FilesystemResourceCollectionTest.php
    │   ├── DirectoryResourceTest.php
    │   ├── FileResourceTest.php
    │   ├── Fixtures/
    │   │   ├── dir1/
    │   │   │   ├── file1
    │   │   │   └── file2
    │   │   ├── dir2/
    │   │   │   ├── .dotfile
    │   │   │   └── file1
    │   │   └── file3
    │   ├── GenericResourceTest.php
    │   ├── Iterator/
    │   │   ├── ResourceCollectionIteratorTest.php
    │   │   └── ResourceFilterIteratorTest.php
    │   ├── LinkResourceTest.php
    │   ├── TestDirectory.php
    │   ├── TestFile.php
    │   └── TestMetadata.php
    ├── StreamWrapper/
    │   └── ResourceStreamWrapperTest.php
    └── Uri/
        └── UriTest.php
Download .txt
SYMBOL INDEX (894 symbols across 86 files)

FILE: src/AbstractEditableRepository.php
  class AbstractEditableRepository (line 26) | abstract class AbstractEditableRepository extends AbstractRepository imp...
    method __construct (line 39) | public function __construct(ChangeStream $changeStream = null)
    method getVersions (line 47) | public function getVersions($path)
    method storeVersion (line 61) | protected function storeVersion(PuliResource $resource)
    method removeVersions (line 73) | protected function removeVersions($path)
    method clearVersions (line 83) | protected function clearVersions()

FILE: src/AbstractJsonRepository.php
  class AbstractJsonRepository (line 44) | abstract class AbstractJsonRepository extends AbstractEditableRepository...
    method __construct (line 101) | public function __construct($path, $baseDirectory, $validateJson = fal...
    method setLogger (line 119) | public function setLogger(LoggerInterface $logger = null)
    method add (line 127) | public function add($path, $resource)
    method get (line 157) | public function get($path)
    method find (line 177) | public function find($query, $language = 'glob')
    method contains (line 195) | public function contains($query, $language = 'glob')
    method remove (line 212) | public function remove($query, $language = 'glob')
    method clear (line 233) | public function clear()
    method listChildren (line 252) | public function listChildren($path)
    method hasChildren (line 277) | public function hasChildren($path)
    method insertReference (line 311) | abstract protected function insertReference($path, $reference);
    method removeReferences (line 318) | abstract protected function removeReferences($glob);
    method getReferencesForPath (line 337) | abstract protected function getReferencesForPath($path);
    method getReferencesForGlob (line 359) | abstract protected function getReferencesForGlob($glob, $flags = 0);
    method getReferencesForRegex (line 383) | abstract protected function getReferencesForRegex($staticPrefix, $rege...
    method getReferencesInDirectory (line 405) | abstract protected function getReferencesInDirectory($path, $flags = 0);
    method log (line 413) | protected function log($level, $message)
    method logReferenceNotFound (line 428) | protected function logReferenceNotFound($path, $reference, $absoluteRe...
    method addFilesystemResource (line 444) | protected function addFilesystemResource($path, FilesystemResource $re...
    method load (line 459) | protected function load()
    method flush (line 489) | protected function flush()
    method isLinkReference (line 524) | protected function isLinkReference($reference)
    method isFilesystemReference (line 537) | protected function isFilesystemReference($reference)
    method createResource (line 550) | protected function createResource($path, $reference)
    method createResources (line 586) | private function createResources(array $references)
    method ensureDirectoryExists (line 600) | private function ensureDirectoryExists($path)
    method addResource (line 621) | private function addResource($path, $resource)
    method getShortClassName (line 653) | private function getShortClassName($className)

FILE: src/AbstractRepository.php
  class AbstractRepository (line 30) | abstract class AbstractRepository implements ResourceRepository
    method getVersions (line 35) | public function getVersions($path)
    method failUnlessGlob (line 50) | protected function failUnlessGlob($language)
    method sanitizePath (line 64) | protected function sanitizePath($path)

FILE: src/Api/ChangeStream/ChangeStream.php
  type ChangeStream (line 26) | interface ChangeStream
    method append (line 33) | public function append(PuliResource $resource);
    method purge (line 40) | public function purge($path);
    method contains (line 49) | public function contains($path);
    method clear (line 54) | public function clear();
    method getVersions (line 67) | public function getVersions($path, ResourceRepository $repository = nu...

FILE: src/Api/ChangeStream/VersionList.php
  class VersionList (line 31) | class VersionList implements IteratorAggregate, ArrayAccess, Countable
    method __construct (line 50) | public function __construct($path, array $versions)
    method getPath (line 65) | public function getPath()
    method getCurrent (line 75) | public function getCurrent()
    method getCurrentVersion (line 85) | public function getCurrentVersion()
    method getFirst (line 95) | public function getFirst()
    method getFirstVersion (line 105) | public function getFirstVersion()
    method contains (line 117) | public function contains($version)
    method get (line 131) | public function get($version)
    method getVersions (line 149) | public function getVersions()
    method toArray (line 159) | public function toArray()
    method getIterator (line 167) | public function getIterator()
    method offsetExists (line 175) | public function offsetExists($offset)
    method offsetGet (line 183) | public function offsetGet($offset)
    method offsetSet (line 191) | public function offsetSet($offset, $value)
    method offsetUnset (line 199) | public function offsetUnset($offset)
    method count (line 207) | public function count()

FILE: src/Api/EditableRepository.php
  type EditableRepository (line 24) | interface EditableRepository extends ResourceRepository
    method add (line 41) | public function add($path, $resource);
    method remove (line 55) | public function remove($query, $language = 'glob');
    method clear (line 62) | public function clear();

FILE: src/Api/NoVersionFoundException.php
  class NoVersionFoundException (line 23) | class NoVersionFoundException extends ResourceNotFoundException
    method forPath (line 33) | public static function forPath($path, Exception $cause = null)

FILE: src/Api/Resource/BodyResource.php
  type BodyResource (line 21) | interface BodyResource extends PuliResource
    method getBody (line 28) | public function getBody();

FILE: src/Api/Resource/FilesystemResource.php
  type FilesystemResource (line 23) | interface FilesystemResource extends PuliResource
    method getFilesystemPath (line 31) | public function getFilesystemPath();

FILE: src/Api/Resource/PuliResource.php
  type PuliResource (line 52) | interface PuliResource extends Serializable
    method getPath (line 65) | public function getPath();
    method getName (line 75) | public function getName();
    method getChild (line 88) | public function getChild($relPath);
    method hasChild (line 97) | public function hasChild($relPath);
    method hasChildren (line 104) | public function hasChildren();
    method listChildren (line 111) | public function listChildren();
    method getVersions (line 118) | public function getVersions();
    method getMetadata (line 125) | public function getMetadata();
    method getRepository (line 137) | public function getRepository();
    method getRepositoryPath (line 151) | public function getRepositoryPath();
    method attachTo (line 174) | public function attachTo(ResourceRepository $repo, $path = null);
    method detach (line 185) | public function detach();
    method isAttached (line 195) | public function isAttached();
    method createReference (line 221) | public function createReference($path);
    method isReference (line 230) | public function isReference();

FILE: src/Api/Resource/ResourceMetadata.php
  class ResourceMetadata (line 21) | class ResourceMetadata
    method getCreationTime (line 30) | public function getCreationTime()
    method getAccessTime (line 42) | public function getAccessTime()
    method getModificationTime (line 54) | public function getModificationTime()
    method getSize (line 66) | public function getSize()

FILE: src/Api/ResourceCollection.php
  type ResourceCollection (line 28) | interface ResourceCollection extends Traversable, ArrayAccess, Countable
    method add (line 35) | public function add(PuliResource $resource);
    method set (line 43) | public function set($key, PuliResource $resource);
    method get (line 54) | public function get($key);
    method remove (line 61) | public function remove($key);
    method has (line 70) | public function has($key);
    method clear (line 75) | public function clear();
    method keys (line 82) | public function keys();
    method replace (line 95) | public function replace($resources);
    method merge (line 108) | public function merge($resources);
    method isEmpty (line 115) | public function isEmpty();
    method getPaths (line 126) | public function getPaths();
    method getNames (line 137) | public function getNames();
    method toArray (line 144) | public function toArray();

FILE: src/Api/ResourceIterator.php
  type ResourceIterator (line 24) | interface ResourceIterator extends Iterator
    method getCurrentResource (line 31) | public function getCurrentResource();

FILE: src/Api/ResourceNotFoundException.php
  class ResourceNotFoundException (line 24) | class ResourceNotFoundException extends RuntimeException
    method forPath (line 34) | public static function forPath($path, Exception $cause = null)

FILE: src/Api/ResourceRepository.php
  type ResourceRepository (line 44) | interface ResourceRepository
    method get (line 58) | public function get($path);
    method getVersions (line 71) | public function getVersions($path);
    method find (line 85) | public function find($query, $language = 'glob');
    method contains (line 99) | public function contains($query, $language = 'glob');
    method hasChildren (line 113) | public function hasChildren($path);
    method listChildren (line 127) | public function listChildren($path);

FILE: src/Api/UnsupportedLanguageException.php
  class UnsupportedLanguageException (line 24) | class UnsupportedLanguageException extends RuntimeException
    method forLanguage (line 34) | public static function forLanguage($language, Exception $cause = null)

FILE: src/Api/UnsupportedOperationException.php
  class UnsupportedOperationException (line 23) | class UnsupportedOperationException extends RuntimeException

FILE: src/Api/UnsupportedResourceException.php
  class UnsupportedResourceException (line 24) | class UnsupportedResourceException extends RuntimeException

FILE: src/ChangeStream/InMemoryChangeStream.php
  class InMemoryChangeStream (line 28) | class InMemoryChangeStream implements ChangeStream
    method append (line 38) | public function append(PuliResource $resource)
    method purge (line 50) | public function purge($path)
    method clear (line 58) | public function clear()
    method contains (line 66) | public function contains($path)
    method getVersions (line 74) | public function getVersions($path, ResourceRepository $repository = null)

FILE: src/ChangeStream/JsonChangeStream.php
  class JsonChangeStream (line 29) | class JsonChangeStream implements ChangeStream
    method __construct (line 49) | public function __construct($path)
    method append (line 60) | public function append(PuliResource $resource)
    method purge (line 78) | public function purge($path)
    method clear (line 92) | public function clear()
    method contains (line 106) | public function contains($path)
    method getVersions (line 118) | public function getVersions($path, ResourceRepository $repository = null)
    method load (line 146) | private function load()
    method flush (line 159) | private function flush()

FILE: src/ChangeStream/KeyValueStoreChangeStream.php
  class KeyValueStoreChangeStream (line 28) | class KeyValueStoreChangeStream implements ChangeStream
    method __construct (line 38) | public function __construct(KeyValueStore $store)
    method append (line 46) | public function append(PuliResource $resource)
    method purge (line 58) | public function purge($path)
    method contains (line 66) | public function contains($path)
    method clear (line 74) | public function clear()
    method getVersions (line 82) | public function getVersions($path, ResourceRepository $repository = null)

FILE: src/Discovery/ResourceBinding.php
  class ResourceBinding (line 29) | class ResourceBinding extends AbstractBinding
    method __construct (line 68) | public function __construct($query, $typeName, array $parameterValues ...
    method getQuery (line 81) | public function getQuery()
    method getLanguage (line 91) | public function getLanguage()
    method getResources (line 101) | public function getResources()
    method setRepository (line 115) | public function setRepository(ResourceRepository $repo)
    method equals (line 123) | public function equals(Binding $other)
    method preSerialize (line 140) | protected function preSerialize(array &$data)
    method postUnserialize (line 151) | protected function postUnserialize(array &$data)

FILE: src/Discovery/ResourceBindingInitializer.php
  class ResourceBindingInitializer (line 24) | class ResourceBindingInitializer implements BindingInitializer
    method __construct (line 34) | public function __construct(ResourceRepository $repo)
    method acceptsBinding (line 42) | public function acceptsBinding($binding)
    method getAcceptedBindingClass (line 52) | public function getAcceptedBindingClass()
    method initializeBinding (line 60) | public function initializeBinding(Binding $binding)

FILE: src/FilesystemRepository.php
  class FilesystemRepository (line 63) | class FilesystemRepository extends AbstractEditableRepository
    method isSymlinkSupported (line 100) | public static function isSymlinkSupported()
    method __construct (line 132) | public function __construct($baseDir = '/', $symlink = true, $relative...
    method get (line 149) | public function get($path)
    method find (line 164) | public function find($query, $language = 'glob')
    method contains (line 172) | public function contains($query, $language = 'glob')
    method hasChildren (line 183) | public function hasChildren($path)
    method listChildren (line 200) | public function listChildren($path)
    method add (line 214) | public function add($path, $resource)
    method remove (line 243) | public function remove($query, $language = 'glob')
    method clear (line 261) | public function clear()
    method ensureDirectoryExists (line 278) | private function ensureDirectoryExists($path)
    method addResource (line 295) | private function addResource($path, PuliResource $resource, $checkPare...
    method removeResource (line 370) | private function removeResource($filesystemPath, &$removed)
    method createResource (line 393) | private function createResource($filesystemPath, $path)
    method iteratorToCollection (line 420) | private function iteratorToCollection(Iterator $iterator)
    method getFilesystemPath (line 441) | private function getFilesystemPath($path)
    method getGlobIterator (line 453) | private function getGlobIterator($query, $language)
    method getDirectoryIterator (line 465) | private function getDirectoryIterator($filesystemPath)
    method symlinkMirror (line 473) | private function symlinkMirror($origin, $target, array $dirsToKeep = a...
    method replaceParentSymlinksByCopies (line 518) | private function replaceParentSymlinksByCopies($path)
    method replaceLinkByCopy (line 554) | private function replaceLinkByCopy($path, array $dirsToKeep = array())
    method trySymlink (line 562) | private function trySymlink($origin, $target)
    method readLink (line 576) | private function readLink($filesystemPath)
    method getPath (line 595) | private function getPath($filesystemPath)

FILE: src/InMemoryRepository.php
  class InMemoryRepository (line 44) | class InMemoryRepository extends AbstractEditableRepository
    method __construct (line 57) | public function __construct(ChangeStream $changeStream = null)
    method get (line 67) | public function get($path)
    method find (line 81) | public function find($query, $language = 'glob')
    method contains (line 100) | public function contains($query, $language = 'glob')
    method add (line 119) | public function add($path, $resource)
    method remove (line 153) | public function remove($query, $language = 'glob')
    method clear (line 171) | public function clear()
    method listChildren (line 190) | public function listChildren($path)
    method hasChildren (line 200) | public function hasChildren($path)
    method ensureDirectoryExists (line 213) | private function ensureDirectoryExists($path)
    method addResource (line 228) | private function addResource($path, PuliResource $resource)
    method removeResource (line 249) | private function removeResource(PuliResource $resource)
    method getChildIterator (line 278) | private function getChildIterator(PuliResource $resource)
    method getGlobIterator (line 298) | protected function getGlobIterator($glob)

FILE: src/JsonRepository.php
  class JsonRepository (line 70) | class JsonRepository extends AbstractJsonRepository implements EditableR...
    method __construct (line 112) | public function __construct($path, $baseDirectory, $validateJson = false)
    method getVersions (line 122) | public function getVersions($path)
    method storeVersion (line 149) | protected function storeVersion(PuliResource $resource)
    method insertReference (line 228) | protected function insertReference($path, $reference)
    method removeReferences (line 256) | protected function removeReferences($glob)
    method getReferencesForPath (line 293) | protected function getReferencesForPath($path)
    method getReferencesForGlob (line 302) | protected function getReferencesForGlob($glob, $flags = 0)
    method getReferencesForRegex (line 318) | protected function getReferencesForRegex($staticPrefix, $regex, $flags...
    method getReferencesInDirectory (line 333) | protected function getReferencesInDirectory($path, $flags = 0)
    method flatten (line 366) | private function flatten(array $references)
    method flattenWithFilter (line 420) | private function flattenWithFilter(array $references, $regex, $flags =...
    method searchReferences (line 537) | private function searchReferences($searchPath, $flags = 0)
    method followLinks (line 750) | private function followLinks(array $references, $flags = 0)
    method appendPathAndFilterExisting (line 811) | private function appendPathAndFilterExisting(array $references, $neste...
    method resolveReferences (line 867) | private function resolveReferences($path, $references, $flags = 0)
    method getPathDepth (line 923) | private function getPathDepth($path)
    method prependOrderEntry (line 938) | private function prependOrderEntry($path, $prependedPath)
    method initWithParentOrder (line 962) | private function initWithParentOrder($path, array $parentReferences)
    method initWithDefaultOrder (line 986) | private function initWithDefaultOrder($path, $insertedPath, $references)

FILE: src/NullRepository.php
  class NullRepository (line 30) | class NullRepository implements EditableRepository
    method add (line 35) | public function add($path, $resource)
    method remove (line 42) | public function remove($query, $language = 'glob')
    method clear (line 49) | public function clear()
    method get (line 56) | public function get($path)
    method getVersions (line 64) | public function getVersions($path)
    method find (line 72) | public function find($query, $language = 'glob')
    method contains (line 80) | public function contains($query, $language = 'glob')
    method hasChildren (line 88) | public function hasChildren($path)
    method listChildren (line 96) | public function listChildren($path)

FILE: src/OptimizedJsonRepository.php
  class OptimizedJsonRepository (line 69) | class OptimizedJsonRepository extends AbstractJsonRepository implements ...
    method clear (line 74) | public function clear()
    method insertReference (line 96) | protected function insertReference($path, $reference)
    method removeReferences (line 104) | protected function removeReferences($glob)
    method getReferencesForPath (line 122) | protected function getReferencesForPath($path)
    method getReferencesForGlob (line 153) | protected function getReferencesForGlob($glob, $flags = 0)
    method getReferencesForRegex (line 169) | protected function getReferencesForRegex($staticPrefix, $regex, $flags...
    method getReferencesInDirectory (line 222) | protected function getReferencesInDirectory($path, $flags = 0)
    method addFilesystemResource (line 236) | protected function addFilesystemResource($path, FilesystemResource $re...

FILE: src/RepositoryFactoryException.php
  class RepositoryFactoryException (line 23) | class RepositoryFactoryException extends RuntimeException

FILE: src/Resource/AbstractFilesystemResource.php
  class AbstractFilesystemResource (line 24) | abstract class AbstractFilesystemResource extends GenericResource implem...
    method __construct (line 37) | public function __construct($filesystemPath, $path = null)
    method getFilesystemPath (line 47) | public function getFilesystemPath()
    method getMetadata (line 55) | public function getMetadata()
    method preSerialize (line 60) | protected function preSerialize(array &$data)
    method postUnserialize (line 67) | protected function postUnserialize(array $data)

FILE: src/Resource/Collection/ArrayResourceCollection.php
  class ArrayResourceCollection (line 31) | class ArrayResourceCollection implements IteratorAggregate, ResourceColl...
    method __construct (line 51) | public function __construct($resources = array())
    method add (line 59) | public function add(PuliResource $resource)
    method set (line 67) | public function set($key, PuliResource $resource)
    method get (line 75) | public function get($key)
    method remove (line 90) | public function remove($key)
    method has (line 98) | public function has($key)
    method clear (line 106) | public function clear()
    method keys (line 114) | public function keys()
    method replace (line 122) | public function replace($resources)
    method merge (line 132) | public function merge($resources)
    method isEmpty (line 145) | public function isEmpty()
    method offsetExists (line 153) | public function offsetExists($key)
    method offsetGet (line 161) | public function offsetGet($key)
    method offsetSet (line 169) | public function offsetSet($key, $value)
    method offsetUnset (line 181) | public function offsetUnset($key)
    method getPaths (line 189) | public function getPaths()
    method getNames (line 202) | public function getNames()
    method count (line 212) | public function count()
    method getIterator (line 217) | public function getIterator($mode = ResourceCollectionIterator::KEY_AS...
    method toArray (line 222) | public function toArray()

FILE: src/Resource/Collection/FilesystemResourceCollection.php
  class FilesystemResourceCollection (line 28) | class FilesystemResourceCollection extends ArrayResourceCollection
    method getFilesystemPaths (line 38) | public function getFilesystemPaths()

FILE: src/Resource/Collection/LazyResourceCollection.php
  class LazyResourceCollection (line 33) | class LazyResourceCollection implements IteratorAggregate, ResourceColle...
    method __construct (line 58) | public function __construct(ResourceRepository $repo, array $paths)
    method add (line 71) | public function add(PuliResource $resource)
    method set (line 86) | public function set($key, PuliResource $resource)
    method get (line 96) | public function get($key)
    method remove (line 119) | public function remove($key)
    method has (line 129) | public function has($key)
    method clear (line 139) | public function clear()
    method keys (line 149) | public function keys()
    method replace (line 166) | public function replace($resources)
    method merge (line 181) | public function merge($resources)
    method isEmpty (line 191) | public function isEmpty()
    method offsetExists (line 199) | public function offsetExists($key)
    method offsetGet (line 207) | public function offsetGet($key)
    method offsetSet (line 220) | public function offsetSet($key, $resource)
    method offsetUnset (line 234) | public function offsetUnset($key)
    method getPaths (line 244) | public function getPaths()
    method getNames (line 261) | public function getNames()
    method getIterator (line 278) | public function getIterator($mode = ResourceCollectionIterator::KEY_AS...
    method toArray (line 290) | public function toArray()
    method count (line 302) | public function count()
    method load (line 310) | private function load()

FILE: src/Resource/DirectoryResource.php
  class DirectoryResource (line 26) | class DirectoryResource extends AbstractFilesystemResource
    method __construct (line 31) | public function __construct($filesystemPath, $path = null)
    method getChild (line 41) | public function getChild($relPath)
    method hasChild (line 64) | public function hasChild($relPath)
    method hasChildren (line 77) | public function hasChildren()
    method listChildren (line 96) | public function listChildren()

FILE: src/Resource/FileResource.php
  class FileResource (line 26) | class FileResource extends AbstractFilesystemResource implements BodyRes...
    method __construct (line 31) | public function __construct($filesystemPath, $path = null)
    method getBody (line 41) | public function getBody()
    method getChild (line 49) | public function getChild($relPath)
    method hasChild (line 57) | public function hasChild($relPath)
    method hasChildren (line 65) | public function hasChildren()
    method listChildren (line 73) | public function listChildren()

FILE: src/Resource/GenericResource.php
  class GenericResource (line 28) | class GenericResource implements PuliResource
    method __construct (line 50) | public function __construct($path = null)
    method getPath (line 59) | public function getPath()
    method getName (line 67) | public function getName()
    method getChild (line 75) | public function getChild($relPath)
    method hasChild (line 87) | public function hasChild($relPath)
    method hasChildren (line 99) | public function hasChildren()
    method listChildren (line 111) | public function listChildren()
    method getVersions (line 129) | public function getVersions()
    method getMetadata (line 141) | public function getMetadata()
    method attachTo (line 149) | public function attachTo(ResourceRepository $repo, $path = null)
    method detach (line 162) | public function detach()
    method getRepository (line 170) | public function getRepository()
    method getRepositoryPath (line 178) | public function getRepositoryPath()
    method isAttached (line 186) | public function isAttached()
    method createReference (line 194) | public function createReference($path)
    method isReference (line 205) | public function isReference()
    method serialize (line 213) | public function serialize()
    method unserialize (line 225) | public function unserialize($string)
    method preSerialize (line 240) | protected function preSerialize(array &$data)
    method postUnserialize (line 255) | protected function postUnserialize(array $data)

FILE: src/Resource/Iterator/RecursiveResourceIterator.php
  type RecursiveResourceIterator (line 26) | interface RecursiveResourceIterator extends ResourceIterator, RecursiveI...

FILE: src/Resource/Iterator/RecursiveResourceIteratorIterator.php
  class RecursiveResourceIteratorIterator (line 43) | class RecursiveResourceIteratorIterator extends RecursiveIteratorIterato...
    method __construct (line 54) | public function __construct(RecursiveResourceIterator $iterator, $mode...
    method getCurrentResource (line 62) | public function getCurrentResource()

FILE: src/Resource/Iterator/ResourceCollectionIterator.php
  class ResourceCollectionIterator (line 55) | class ResourceCollectionIterator implements RecursiveResourceIterator
    method __construct (line 118) | public function __construct(ResourceCollection $resources, $mode = null)
    method current (line 138) | public function current()
    method next (line 154) | public function next()
    method key (line 166) | public function key()
    method valid (line 184) | public function valid()
    method rewind (line 192) | public function rewind()
    method hasChildren (line 203) | public function hasChildren()
    method getChildren (line 214) | public function getChildren()
    method getCurrentResource (line 222) | public function getCurrentResource()

FILE: src/Resource/Iterator/ResourceFilterIterator.php
  class ResourceFilterIterator (line 43) | class ResourceFilterIterator extends FilterIterator implements ResourceI...
    method __construct (line 110) | public function __construct(ResourceIterator $iterator, $pattern, $mod...
    method accept (line 134) | public function accept()
    method getCurrentResource (line 154) | public function getCurrentResource()

FILE: src/Resource/LinkResource.php
  class LinkResource (line 26) | class LinkResource extends GenericResource implements PuliResource
    method __construct (line 37) | public function __construct($targetPath, $path = null)
    method getTargetPath (line 47) | public function getTargetPath()
    method getTarget (line 55) | public function getTarget()
    method getChild (line 67) | public function getChild($relPath)
    method hasChild (line 79) | public function hasChild($relPath)
    method hasChildren (line 91) | public function hasChildren()
    method listChildren (line 103) | public function listChildren()
    method preSerialize (line 118) | protected function preSerialize(array &$data)
    method postUnserialize (line 125) | protected function postUnserialize(array $data)

FILE: src/Resource/Metadata/FilesystemMetadata.php
  class FilesystemMetadata (line 23) | class FilesystemMetadata extends ResourceMetadata
    method __construct (line 27) | public function __construct($filesystemPath)
    method getCreationTime (line 35) | public function getCreationTime()
    method getAccessTime (line 52) | public function getAccessTime()
    method getModificationTime (line 63) | public function getModificationTime()
    method getSize (line 74) | public function getSize()
    method fixWindowsPath (line 90) | private function fixWindowsPath($path)

FILE: src/StreamWrapper/ResourceStreamWrapper.php
  class ResourceStreamWrapper (line 49) | class ResourceStreamWrapper implements StreamWrapper
    method register (line 176) | public static function register($scheme, $repositoryFactory)
    method unregister (line 210) | public static function unregister($scheme)
    method dir_opendir (line 226) | public function dir_opendir($uri, $options)
    method dir_closedir (line 246) | public function dir_closedir()
    method dir_readdir (line 258) | public function dir_readdir()
    method dir_rewinddir (line 276) | public function dir_rewinddir()
    method mkdir (line 288) | public function mkdir($uri, $mode, $options)
    method rename (line 302) | public function rename($uriFrom, $uriTo)
    method rmdir (line 322) | public function rmdir($uri, $options)
    method stream_cast (line 344) | public function stream_cast($castAs)
    method stream_close (line 354) | public function stream_close()
    method stream_eof (line 366) | public function stream_eof()
    method stream_flush (line 378) | public function stream_flush()
    method stream_lock (line 390) | public function stream_lock($operation)
    method stream_metadata (line 403) | public function stream_metadata($uri, $option)
    method stream_open (line 443) | public function stream_open($uri, $mode, $options, &$openedPath)
    method stream_read (line 485) | public function stream_read($length)
    method stream_seek (line 497) | public function stream_seek($offset, $whence = SEEK_SET)
    method stream_set_option (line 509) | public function stream_set_option($option, $arg1, $arg2)
    method stream_stat (line 519) | public function stream_stat()
    method stream_tell (line 531) | public function stream_tell()
    method stream_truncate (line 543) | public function stream_truncate($newSize)
    method stream_write (line 555) | public function stream_write($data)
    method unlink (line 567) | public function unlink($uri)
    method url_stat (line 581) | public function url_stat($uri, $flags)
    method getRepository (line 629) | private function getRepository($scheme)

FILE: src/StreamWrapper/StreamWrapper.php
  type StreamWrapper (line 23) | interface StreamWrapper
    method dir_closedir (line 25) | public function dir_closedir();
    method dir_opendir (line 27) | public function dir_opendir($url, $options);
    method dir_readdir (line 29) | public function dir_readdir();
    method dir_rewinddir (line 31) | public function dir_rewinddir();
    method mkdir (line 33) | public function mkdir($url, $mode, $options);
    method rename (line 35) | public function rename($urlFrom, $urlTo);
    method rmdir (line 37) | public function rmdir($url, $options);
    method stream_cast (line 39) | public function stream_cast($castAs);
    method stream_close (line 41) | public function stream_close();
    method stream_eof (line 43) | public function stream_eof();
    method stream_flush (line 45) | public function stream_flush();
    method stream_lock (line 47) | public function stream_lock($operation);
    method stream_open (line 49) | public function stream_open($url, $mode, $options, &$openedPath);
    method stream_read (line 51) | public function stream_read($length);
    method stream_seek (line 53) | public function stream_seek($offset, $whence = SEEK_SET);
    method stream_set_option (line 55) | public function stream_set_option($option, $arg1, $arg2);
    method stream_stat (line 57) | public function stream_stat();
    method stream_tell (line 59) | public function stream_tell();
    method stream_write (line 61) | public function stream_write($data);
    method unlink (line 63) | public function unlink($url);
    method url_stat (line 65) | public function url_stat($url, $flags);

FILE: src/StreamWrapper/StreamWrapperException.php
  class StreamWrapperException (line 23) | class StreamWrapperException extends RuntimeException

FILE: src/Uri/InvalidUriException.php
  class InvalidUriException (line 23) | class InvalidUriException extends RuntimeException

FILE: src/Uri/Uri.php
  class Uri (line 21) | final class Uri
    method parse (line 47) | public static function parse($uri)
    method __construct (line 98) | private function __construct()

FILE: tests/AbstractEditableRepositoryTest.php
  class AbstractEditableRepositoryTest (line 27) | abstract class AbstractEditableRepositoryTest extends AbstractRepository...
    method setUpBeforeClass (line 53) | public static function setUpBeforeClass()
    method createWriteRepository (line 70) | abstract protected function createWriteRepository();
    method createReadRepository (line 77) | abstract protected function createReadRepository(EditableRepository $w...
    method setUp (line 79) | protected function setUp()
    method markAsSkippedIfSymlinkIsMissing (line 88) | protected function markAsSkippedIfSymlinkIsMissing()
    method testRootIsEmptyBeforeAdding (line 99) | public function testRootIsEmptyBeforeAdding()
    method testAddFile (line 108) | public function testAddFile()
    method testAddDoesNotAttachResourceToRepository (line 126) | public function testAddDoesNotAttachResourceToRepository()
    method testAddDoesNotChangeAttachedRepository (line 150) | public function testAddDoesNotChangeAttachedRepository()
    method testAddMergesResourceChildren (line 177) | public function testAddMergesResourceChildren()
    method testAddDot (line 213) | public function testAddDot()
    method testAddDotDot (line 223) | public function testAddDotDot()
    method testAddTrimsTrailingSlash (line 233) | public function testAddTrimsTrailingSlash()
    method testAddCollection (line 243) | public function testAddCollection()
    method testAddRoot (line 260) | public function testAddRoot()
    method testAddExpectsAbsolutePath (line 291) | public function testAddExpectsAbsolutePath()
    method testAddExpectsNonEmptyPath (line 299) | public function testAddExpectsNonEmptyPath()
    method testAddExpectsStringPath (line 307) | public function testAddExpectsStringPath()
    method testAddExpectsResource (line 315) | public function testAddExpectsResource()
    method testOverride (line 320) | public function testOverride()
    method testOverrideSuperPath (line 352) | public function testOverrideSuperPath()
    method testOverrideSubPath (line 392) | public function testOverrideSubPath()
    method testOverrideSuperAndSubPathShortFirst (line 436) | public function testOverrideSuperAndSubPathShortFirst()
    method testOverrideSuperAndSubPathMediumFirst (line 493) | public function testOverrideSuperAndSubPathMediumFirst()
    method testOverrideSuperAndSubPathLongFirst (line 550) | public function testOverrideSuperAndSubPathLongFirst()
    method testOverrideFourLevels (line 607) | public function testOverrideFourLevels()
    method testRemoveFile (line 662) | public function testRemoveFile()
    method testRemoveMany (line 682) | public function testRemoveMany()
    method provideDirectoryGlob (line 700) | public function provideDirectoryGlob()
    method testRemoveDirectory (line 711) | public function testRemoveDirectory($glob)
    method testRemoveDot (line 731) | public function testRemoveDot()
    method testRemoveDotDot (line 751) | public function testRemoveDotDot()
    method testRemoveDiscardsTrailingSlash (line 773) | public function testRemoveDiscardsTrailingSlash()
    method testCannotRemoveRoot (line 794) | public function testCannotRemoveRoot()
    method testRemoveInterpretsConsecutiveSlashesAsRoot (line 802) | public function testRemoveInterpretsConsecutiveSlashesAsRoot()
    method testRemoveExpectsAbsolutePath (line 810) | public function testRemoveExpectsAbsolutePath()
    method testRemoveExpectsNonEmptyPath (line 818) | public function testRemoveExpectsNonEmptyPath()
    method testRemoveExpectsStringPath (line 826) | public function testRemoveExpectsStringPath()
    method testClear (line 831) | public function testClear()
    method testFileLink (line 853) | public function testFileLink()
    method testDirectoryLink (line 878) | public function testDirectoryLink()
    method testGetVersionsFailsForDeletedResources (line 901) | public function testGetVersionsFailsForDeletedResources()
    method testGetVersionsFailsForChildrenOfDeletedResources (line 914) | public function testGetVersionsFailsForChildrenOfDeletedResources()
    method testGetVersionsFailsAfterClearing (line 926) | public function testGetVersionsFailsAfterClearing()
    method testGetVersionsSucceedsForRootAfterClearing (line 934) | public function testGetVersionsSucceedsForRootAfterClearing()
    method testGetVersionsDoesNotIncludeDeletedResources (line 941) | public function testGetVersionsDoesNotIncludeDeletedResources()

FILE: tests/AbstractFilesystemRepositorySymlinkTest.php
  class AbstractFilesystemRepositorySymlinkTest (line 21) | abstract class AbstractFilesystemRepositorySymlinkTest extends AbstractF...
    method setUp (line 33) | protected function setUp()
    method tearDown (line 53) | protected function tearDown()
    method testClearDirectoryLinksDoesNotRemoveChildrenFiles (line 61) | public function testClearDirectoryLinksDoesNotRemoveChildrenFiles()

FILE: tests/AbstractFilesystemRepositoryTest.php
  class AbstractFilesystemRepositoryTest (line 17) | abstract class AbstractFilesystemRepositoryTest extends AbstractEditable...
    method assertPathsAreEqual (line 19) | protected function assertPathsAreEqual($expected, $actual)

FILE: tests/AbstractJsonRepositoryTest.php
  class AbstractJsonRepositoryTest (line 27) | abstract class AbstractJsonRepositoryTest extends AbstractEditableReposi...
    method setUp (line 67) | protected function setUp()
    method tearDown (line 83) | protected function tearDown()
    method testContainsFailsIfLanguageNotGlob (line 95) | public function testContainsFailsIfLanguageNotGlob()
    method testFindFailsIfLanguageNotGlob (line 104) | public function testFindFailsIfLanguageNotGlob()
    method testRemoveFailsIfLanguageNotGlob (line 113) | public function testRemoveFailsIfLanguageNotGlob()
    method testGetLogsWarningIfReferenceNotFound (line 121) | public function testGetLogsWarningIfReferenceNotFound()
    method testFindLogsWarningIfReferenceNotFound (line 137) | public function testFindLogsWarningIfReferenceNotFound()
    method testContainsLogsWarningIfReferenceNotFound (line 153) | public function testContainsLogsWarningIfReferenceNotFound()
    method testHasChildrenLogsWarningIfReferenceNotFound (line 169) | public function testHasChildrenLogsWarningIfReferenceNotFound()
    method testListChildrenLogsWarningIfReferenceNotFound (line 185) | public function testListChildrenLogsWarningIfReferenceNotFound()
    method prepareFixtures (line 201) | protected function prepareFixtures(PuliResource $root)
    method copyToFilesystem (line 212) | private function copyToFilesystem($resource, $parentPath = '')

FILE: tests/AbstractRepositoryTest.php
  class AbstractRepositoryTest (line 23) | abstract class AbstractRepositoryTest extends PHPUnit_Framework_TestCase
    method createPrefilledRepository (line 30) | abstract protected function createPrefilledRepository(PuliResource $ro...
    method createFile (line 38) | protected function createFile($path = null, $body = TestFile::BODY)
    method createDirectory (line 49) | protected function createDirectory($path = null, array $children = arr...
    method prepareFixtures (line 61) | protected function prepareFixtures(PuliResource $root)
    method pass (line 66) | protected function pass()
    method testContainsPath (line 71) | public function testContainsPath()
    method testContainsPattern (line 136) | public function testContainsPattern()
    method testContainsDiscardsTrailingSlash (line 162) | public function testContainsDiscardsTrailingSlash()
    method testContainsInterpretsConsecutiveSlashesAsRoot (line 171) | public function testContainsInterpretsConsecutiveSlashesAsRoot()
    method testContainsExpectsAbsolutePath (line 181) | public function testContainsExpectsAbsolutePath()
    method testContainsExpectsNonEmptyPath (line 193) | public function testContainsExpectsNonEmptyPath()
    method testContainsExpectsStringPath (line 203) | public function testContainsExpectsStringPath()
    method testGetResource (line 210) | public function testGetResource()
    method testGetBodyResource (line 226) | public function testGetBodyResource()
    method testGetDiscardsTrailingSlash (line 244) | public function testGetDiscardsTrailingSlash()
    method testGetInterpretsConsecutiveSlashesAsRoot (line 253) | public function testGetInterpretsConsecutiveSlashesAsRoot()
    method testGetCanonicalizesFilePaths (line 260) | public function testGetCanonicalizesFilePaths()
    method testGetCanonicalizesDirectoryPaths (line 273) | public function testGetCanonicalizesDirectoryPaths()
    method testGetExpectsExistingResource (line 289) | public function testGetExpectsExistingResource()
    method testGetExpectsAbsolutePath (line 299) | public function testGetExpectsAbsolutePath()
    method testGetExpectsNonEmptyPath (line 311) | public function testGetExpectsNonEmptyPath()
    method testGetExpectsStringPath (line 321) | public function testGetExpectsStringPath()
    method testGetDotInDirectory (line 328) | public function testGetDotInDirectory()
    method testGetDotInFile (line 337) | public function testGetDotInFile()
    method testGetDotInRoot (line 354) | public function testGetDotInRoot()
    method testGetDotDotInDirectory (line 361) | public function testGetDotDotInDirectory()
    method testGetDotDotInFile (line 372) | public function testGetDotDotInFile()
    method testGetDotDotInRoot (line 389) | public function testGetDotDotInRoot()
    method testHasChildren (line 396) | public function testHasChildren()
    method testHasChildrenExpectsExistingResource (line 421) | public function testHasChildrenExpectsExistingResource()
    method testHasChildrenExpectsAbsolutePath (line 431) | public function testHasChildrenExpectsAbsolutePath()
    method testHasChildrenExpectsNonEmptyPath (line 443) | public function testHasChildrenExpectsNonEmptyPath()
    method testHasChildrenExpectsStringPath (line 453) | public function testHasChildrenExpectsStringPath()
    method testListChildren (line 460) | public function testListChildren()
    method testListChildrenReturnsEmptyCollectionForFiles (line 492) | public function testListChildrenReturnsEmptyCollectionForFiles()
    method testListRoot (line 508) | public function testListRoot()
    method testListChildrenExpectsExistingResource (line 527) | public function testListChildrenExpectsExistingResource()
    method testListChildrenExpectsAbsolutePath (line 537) | public function testListChildrenExpectsAbsolutePath()
    method testListChildrenExpectsNonEmptyPath (line 549) | public function testListChildrenExpectsNonEmptyPath()
    method testListChildrenExpectsStringPath (line 559) | public function testListChildrenExpectsStringPath()
    method testFind (line 566) | public function testFind()
    method testFindBrackets (line 589) | public function testFindBrackets()
    method testFindFull (line 611) | public function testFindFull()
    method testFindFile (line 635) | public function testFindFile()
    method testFindDirectory (line 652) | public function testFindDirectory()
    method testFindCanonicalizesGlob (line 665) | public function testFindCanonicalizesGlob()
    method testFindNoMatches (line 682) | public function testFindNoMatches()
    method testFindExpectsAbsolutePath (line 695) | public function testFindExpectsAbsolutePath()
    method testFindExpectsNonEmptyPath (line 705) | public function testFindExpectsNonEmptyPath()
    method testFindExpectsStringPath (line 715) | public function testFindExpectsStringPath()
    method testGetVersions (line 722) | public function testGetVersions()
    method testGetRootVersions (line 743) | public function testGetRootVersions()
    method testGetVersionsFailsIfNoneFound (line 760) | public function testGetVersionsFailsIfNoneFound()

FILE: tests/Api/ChangeStream/VersionListTest.php
  class VersionListTest (line 23) | class VersionListTest extends PHPUnit_Framework_TestCase
    method testFailIfEmptyPath (line 28) | public function testFailIfEmptyPath()
    method testFailIfInvalidPath (line 36) | public function testFailIfInvalidPath()
    method testFailIfNoVersion (line 44) | public function testFailIfNoVersion()
    method testFailIfInvalidVersions (line 52) | public function testFailIfInvalidVersions()
    method testGetCurrentVersion (line 57) | public function testGetCurrentVersion()
    method testGetCurrent (line 69) | public function testGetCurrent()
    method testGetFirstVersion (line 81) | public function testGetFirstVersion()
    method testGetFirst (line 93) | public function testGetFirst()
    method testGet (line 105) | public function testGet()
    method testGetFailsIfNotFound (line 123) | public function testGetFailsIfNotFound()
    method testGetVersions (line 130) | public function testGetVersions()
    method testCount (line 142) | public function testCount()
    method testIterate (line 154) | public function testIterate()
    method testToArray (line 166) | public function testToArray()
    method testArrayAccess (line 178) | public function testArrayAccess()
    method getMockResource (line 198) | private function getMockResource()

FILE: tests/ChangeStream/AbstractChangeStreamTest.php
  class AbstractChangeStreamTest (line 22) | abstract class AbstractChangeStreamTest extends PHPUnit_Framework_TestCase
    method setUp (line 44) | protected function setUp()
    method createWriteStream (line 55) | abstract protected function createWriteStream();
    method createReadStream (line 62) | abstract protected function createReadStream(ChangeStream $writeStream);
    method testAppend (line 64) | public function testAppend()
    method testContains (line 80) | public function testContains()
    method testPurge (line 91) | public function testPurge()
    method testClear (line 103) | public function testClear()
    method testAppendAfterPurging (line 115) | public function testAppendAfterPurging()
    method testGetVersionsFailsIfNotFound (line 130) | public function testGetVersionsFailsIfNotFound()
    method testGetVersionsFailsAfterPurging (line 138) | public function testGetVersionsFailsAfterPurging()
    method testResourcesNotAttachedToRepositoryByDefault (line 148) | public function testResourcesNotAttachedToRepositoryByDefault()
    method testResourcesAttachedToRepositoryIfPassed (line 160) | public function testResourcesAttachedToRepositoryIfPassed()
    method testResourcesInStreamRemainDetached (line 172) | public function testResourcesInStreamRemainDetached()

FILE: tests/ChangeStream/InMemoryChangeStreamTest.php
  class InMemoryChangeStreamTest (line 20) | class InMemoryChangeStreamTest extends AbstractChangeStreamTest
    method createWriteStream (line 22) | protected function createWriteStream()
    method createReadStream (line 27) | protected function createReadStream(ChangeStream $writeStream)

FILE: tests/ChangeStream/JsonChangeStreamLoadedTest.php
  class JsonChangeStreamLoadedTest (line 21) | class JsonChangeStreamLoadedTest extends JsonChangeStreamTest
    method createReadStream (line 23) | protected function createReadStream(ChangeStream $writeStream)

FILE: tests/ChangeStream/JsonChangeStreamTest.php
  class JsonChangeStreamTest (line 23) | class JsonChangeStreamTest extends AbstractChangeStreamTest
    method setUp (line 35) | protected function setUp()
    method createWriteStream (line 43) | protected function createWriteStream()
    method createReadStream (line 48) | protected function createReadStream(ChangeStream $writeStream)

FILE: tests/ChangeStream/KeyValueStoreChangeStreamTest.php
  class KeyValueStoreChangeStreamTest (line 23) | class KeyValueStoreChangeStreamTest extends AbstractChangeStreamTest
    method setUp (line 30) | protected function setUp()
    method createWriteStream (line 37) | protected function createWriteStream()
    method createReadStream (line 47) | protected function createReadStream(ChangeStream $writeStream)

FILE: tests/Discovery/Fixtures/SubResourceBinding.php
  class SubResourceBinding (line 19) | class SubResourceBinding extends ResourceBinding

FILE: tests/Discovery/ResourceBindingInitializerTest.php
  class ResourceBindingInitializerTest (line 26) | class ResourceBindingInitializerTest extends PHPUnit_Framework_TestCase
    method setUp (line 44) | protected function setUp()
    method testAcceptsBinding (line 50) | public function testAcceptsBinding()
    method testAcceptsBindingAcceptsSubClasses (line 58) | public function testAcceptsBindingAcceptsSubClasses()
    method testGetAcceptedBindingClass (line 63) | public function testGetAcceptedBindingClass()
    method testInitializeBinding (line 68) | public function testInitializeBinding()
    method testInitializeBindingOfSubClass (line 81) | public function testInitializeBindingOfSubClass()
    method testInitializeBindingFailsIfInvalidArgument (line 97) | public function testInitializeBindingFailsIfInvalidArgument()

FILE: tests/Discovery/ResourceBindingTest.php
  class ResourceBindingTest (line 22) | class ResourceBindingTest extends AbstractBindingTest
    method createBinding (line 24) | protected function createBinding($typeName, array $parameterValues = a...
    method testCreateWithQuery (line 29) | public function testCreateWithQuery()
    method testGetResources (line 38) | public function testGetResources()
    method testGetResourcesFailsIfNotSet (line 56) | public function testGetResourcesFailsIfNotSet()

FILE: tests/FilesystemRepositoryAbsoluteSymlinkTest.php
  class FilesystemRepositoryAbsoluteSymlinkTest (line 23) | class FilesystemRepositoryAbsoluteSymlinkTest extends AbstractFilesystem...
    method createPrefilledRepository (line 25) | protected function createPrefilledRepository(PuliResource $root)
    method createWriteRepository (line 33) | protected function createWriteRepository()
    method createReadRepository (line 38) | protected function createReadRepository(EditableRepository $writeRepo)
    method testAddDirectoryCreatesSymlink (line 43) | public function testAddDirectoryCreatesSymlink()
    method testOverwriteDirectoryWithDirectoryTurnsSymlinkIntoDirectory (line 51) | public function testOverwriteDirectoryWithDirectoryTurnsSymlinkIntoDir...
    method testOverwriteDirectoryWithDirectoryMergesSubdirectories (line 68) | public function testOverwriteDirectoryWithDirectoryMergesSubdirectories()
    method testOverwriteDirectoryWithFileReplacesSymlink (line 86) | public function testOverwriteDirectoryWithFileReplacesSymlink()
    method testAddFileCreatesSymlink (line 95) | public function testAddFileCreatesSymlink()
    method testOverwriteFileWithDirectoryReplacesSymlink (line 103) | public function testOverwriteFileWithDirectoryReplacesSymlink()
    method testOverwriteFileWithFileReplacesSymlink (line 112) | public function testOverwriteFileWithFileReplacesSymlink()
    method testAddSubDirectoryTurnsParentSymlinkIntoDirectory (line 121) | public function testAddSubDirectoryTurnsParentSymlinkIntoDirectory()
    method testAddSubFileTurnsParentSymlinkIntoDirectory (line 138) | public function testAddSubFileTurnsParentSymlinkIntoDirectory()
    method testAddSubResourceWithBodyTurnsParentSymlinkIntoDirectory (line 155) | public function testAddSubResourceWithBodyTurnsParentSymlinkIntoDirect...
    method testAddSubSubDirectoryTurnsParentSymlinkIntoDirectory (line 172) | public function testAddSubSubDirectoryTurnsParentSymlinkIntoDirectory()
    method testAddSubSubFileTurnsParentSymlinkIntoDirectory (line 189) | public function testAddSubSubFileTurnsParentSymlinkIntoDirectory()
    method testAddSubSubResourceWithBodyTurnsParentSymlinkIntoDirectory (line 206) | public function testAddSubSubResourceWithBodyTurnsParentSymlinkIntoDir...

FILE: tests/FilesystemRepositoryCopyTest.php
  class FilesystemRepositoryCopyTest (line 26) | class FilesystemRepositoryCopyTest extends AbstractEditableRepositoryTest
    method setUp (line 30) | protected function setUp()
    method tearDown (line 37) | protected function tearDown()
    method createPrefilledRepository (line 45) | protected function createPrefilledRepository(PuliResource $root)
    method createWriteRepository (line 53) | protected function createWriteRepository()
    method createReadRepository (line 58) | protected function createReadRepository(EditableRepository $writeRepo)
    method testPassNonExistingBaseDirectory (line 66) | public function testPassNonExistingBaseDirectory()
    method testPassFileAsBaseDirectory (line 74) | public function testPassFileAsBaseDirectory()
    method testGetFileLink (line 81) | public function testGetFileLink()
    method testGetDirectoryLink (line 94) | public function testGetDirectoryLink()
    method testContainsFailsIfLanguageNotGlob (line 111) | public function testContainsFailsIfLanguageNotGlob()
    method testFindFailsIfLanguageNotGlob (line 120) | public function testFindFailsIfLanguageNotGlob()
    method testFailIfAddedResourceHasBodyAndChildren (line 128) | public function testFailIfAddedResourceHasBodyAndChildren()
    method testAddDirectory (line 139) | public function testAddDirectory()
    method testAddFile (line 152) | public function testAddFile()
    method testFailIfAddingFileAsChildOfFile (line 164) | public function testFailIfAddingFileAsChildOfFile()
    method testRemoveFailsIfLanguageNotGlob (line 174) | public function testRemoveFailsIfLanguageNotGlob()
    method testFileLink (line 182) | public function testFileLink()
    method testDirectoryLink (line 190) | public function testDirectoryLink()

FILE: tests/FilesystemRepositoryLoadedTest.php
  class FilesystemRepositoryLoadedTest (line 24) | class FilesystemRepositoryLoadedTest extends AbstractEditableRepositoryTest
    method setUp (line 28) | protected function setUp()
    method tearDown (line 35) | protected function tearDown()
    method createPrefilledRepository (line 43) | protected function createPrefilledRepository(PuliResource $root)
    method createWriteRepository (line 51) | protected function createWriteRepository()
    method createReadRepository (line 56) | protected function createReadRepository(EditableRepository $writeRepo)
    method testFileLink (line 64) | public function testFileLink()
    method testDirectoryLink (line 72) | public function testDirectoryLink()

FILE: tests/FilesystemRepositoryRelativeSymlinkTest.php
  class FilesystemRepositoryRelativeSymlinkTest (line 23) | class FilesystemRepositoryRelativeSymlinkTest extends AbstractFilesystem...
    method setUp (line 25) | protected function setUp()
    method createPrefilledRepository (line 34) | protected function createPrefilledRepository(PuliResource $root)
    method createWriteRepository (line 42) | protected function createWriteRepository()
    method createReadRepository (line 47) | protected function createReadRepository(EditableRepository $writeRepo)
    method testAddDirectoryCreatesSymlink (line 52) | public function testAddDirectoryCreatesSymlink()
    method testOverwriteDirectoryWithDirectoryTurnsSymlinkIntoDirectory (line 60) | public function testOverwriteDirectoryWithDirectoryTurnsSymlinkIntoDir...
    method testOverwriteDirectoryWithDirectoryMergesSubdirectories (line 77) | public function testOverwriteDirectoryWithDirectoryMergesSubdirectories()
    method testOverwriteDirectoryWithFileReplacesSymlink (line 95) | public function testOverwriteDirectoryWithFileReplacesSymlink()
    method testAddFileCreatesSymlink (line 104) | public function testAddFileCreatesSymlink()
    method testOverwriteFileWithDirectoryReplacesSymlink (line 112) | public function testOverwriteFileWithDirectoryReplacesSymlink()
    method testOverwriteFileWithFileReplacesSymlink (line 121) | public function testOverwriteFileWithFileReplacesSymlink()
    method testAddSubDirectoryTurnsParentSymlinkIntoDirectory (line 130) | public function testAddSubDirectoryTurnsParentSymlinkIntoDirectory()
    method testAddSubFileTurnsParentSymlinkIntoDirectory (line 147) | public function testAddSubFileTurnsParentSymlinkIntoDirectory()
    method testAddSubResourceWithBodyTurnsParentSymlinkIntoDirectory (line 164) | public function testAddSubResourceWithBodyTurnsParentSymlinkIntoDirect...
    method testAddSubSubDirectoryTurnsParentSymlinkIntoDirectory (line 181) | public function testAddSubSubDirectoryTurnsParentSymlinkIntoDirectory()
    method testAddSubSubFileTurnsParentSymlinkIntoDirectory (line 198) | public function testAddSubSubFileTurnsParentSymlinkIntoDirectory()
    method testAddSubSubResourceWithBodyTurnsParentSymlinkIntoDirectory (line 215) | public function testAddSubSubResourceWithBodyTurnsParentSymlinkIntoDir...

FILE: tests/InMemoryRepositoryTest.php
  class InMemoryRepositoryTest (line 21) | class InMemoryRepositoryTest extends AbstractEditableRepositoryTest
    method setUp (line 28) | protected function setUp()
    method createPrefilledRepository (line 35) | protected function createPrefilledRepository(PuliResource $root)
    method createWriteRepository (line 43) | protected function createWriteRepository()
    method createReadRepository (line 48) | protected function createReadRepository(EditableRepository $writeRepo)
    method testContainsFailsIfLanguageNotGlob (line 57) | public function testContainsFailsIfLanguageNotGlob()
    method testFindFailsIfLanguageNotGlob (line 66) | public function testFindFailsIfLanguageNotGlob()
    method testRemoveFailsIfLanguageNotGlob (line 75) | public function testRemoveFailsIfLanguageNotGlob()

FILE: tests/JsonRepositoryLoadedTest.php
  class JsonRepositoryLoadedTest (line 20) | class JsonRepositoryLoadedTest extends JsonRepositoryTest
    method createReadRepository (line 22) | protected function createReadRepository(EditableRepository $writeRepo)

FILE: tests/JsonRepositoryTest.php
  class JsonRepositoryTest (line 25) | class JsonRepositoryTest extends AbstractJsonRepositoryTest
    method createPrefilledRepository (line 27) | protected function createPrefilledRepository(PuliResource $root)
    method createWriteRepository (line 35) | protected function createWriteRepository()
    method createReadRepository (line 40) | protected function createReadRepository(EditableRepository $writeRepo)
    method testRemoveFailsWhenPassingPathsThatAreNotMappings (line 48) | public function testRemoveFailsWhenPassingPathsThatAreNotMappings()
    method testGetVersionsLogsWarningIfReferenceNotFound (line 58) | public function testGetVersionsLogsWarningIfReferenceNotFound()

FILE: tests/NullRepositoryTest.php
  class NullRepositoryTest (line 22) | class NullRepositoryTest extends PHPUnit_Framework_TestCase
    method setUp (line 29) | protected function setUp()
    method testAdd (line 34) | public function testAdd()
    method testRemove (line 41) | public function testRemove()
    method testFind (line 50) | public function testFind()
    method testListChildren (line 57) | public function testListChildren()
    method testGetAlwaysThrowsException (line 70) | public function testGetAlwaysThrowsException()
    method testGetVersionsAlwaysThrowsException (line 80) | public function testGetVersionsAlwaysThrowsException()

FILE: tests/OptimizedJsonRepositoryLoadedTest.php
  class OptimizedJsonRepositoryLoadedTest (line 20) | class OptimizedJsonRepositoryLoadedTest extends OptimizedJsonRepositoryTest
    method createReadRepository (line 22) | protected function createReadRepository(EditableRepository $writeRepo)

FILE: tests/OptimizedJsonRepositoryTest.php
  class OptimizedJsonRepositoryTest (line 22) | class OptimizedJsonRepositoryTest extends AbstractJsonRepositoryTest
    method createPrefilledRepository (line 24) | protected function createPrefilledRepository(PuliResource $root)
    method createWriteRepository (line 32) | protected function createWriteRepository()
    method createReadRepository (line 37) | protected function createReadRepository(EditableRepository $writeRepo)

FILE: tests/Resource/AbstractFilesystemResourceTest.php
  class AbstractFilesystemResourceTest (line 20) | abstract class AbstractFilesystemResourceTest extends AbstractResourceTest
    method setUp (line 24) | protected function setUp()
    method createResource (line 36) | protected function createResource($path = null)
    method createFilesystemResource (line 47) | abstract protected function createFilesystemResource($filesystemPath, ...
    method getValidFilesystemPath (line 49) | abstract protected function getValidFilesystemPath();
    method getValidFilesystemPath2 (line 51) | abstract protected function getValidFilesystemPath2();
    method getValidFilesystemPath3 (line 53) | abstract protected function getValidFilesystemPath3();
    method getInvalidFilesystemPaths (line 55) | abstract public function getInvalidFilesystemPaths();
    method testFailIfNonExistingFile (line 61) | public function testFailIfNonExistingFile($filesystemPath)
    method testGetFilesystemPath (line 66) | public function testGetFilesystemPath()
    method testAttachDoesNotChangeFilesystemPath (line 74) | public function testAttachDoesNotChangeFilesystemPath()
    method testDetachDoesNotChangeFilesystemPath (line 83) | public function testDetachDoesNotChangeFilesystemPath()
    method testSerializeKeepsFilesystemPath (line 93) | public function testSerializeKeepsFilesystemPath()
    method assertPathsAreEqual (line 103) | protected function assertPathsAreEqual($expected, $actual)

FILE: tests/Resource/AbstractResourceTest.php
  class AbstractResourceTest (line 24) | abstract class AbstractResourceTest extends PHPUnit_Framework_TestCase
    method createResource (line 36) | abstract protected function createResource($path = null);
    method setUp (line 38) | protected function setUp()
    method testCreate (line 43) | public function testCreate()
    method testCreateWithPath (line 55) | public function testCreateWithPath()
    method testAttach (line 67) | public function testAttach()
    method testAttachDoesNotChangePath (line 80) | public function testAttachDoesNotChangePath()
    method testAttachSetsPathIfGiven (line 93) | public function testAttachSetsPathIfGiven()
    method testReattach (line 106) | public function testReattach()
    method testDetach (line 122) | public function testDetach()
    method testDetachKeepsPath (line 136) | public function testDetachKeepsPath()
    method testCreateReferenceToDetachedResource (line 150) | public function testCreateReferenceToDetachedResource()
    method testCreateReferenceToDetachedResourceWithPath (line 171) | public function testCreateReferenceToDetachedResourceWithPath()
    method testCreateReferenceToAttachedResource (line 192) | public function testCreateReferenceToAttachedResource()
    method testAttachDetachedReference (line 214) | public function testAttachDetachedReference()
    method testAttachDetachedReferenceWithPath (line 239) | public function testAttachDetachedReferenceWithPath()
    method testReattachAttachedReference (line 255) | public function testReattachAttachedReference()
    method testReattachAttachedReferenceWithPath (line 282) | public function testReattachAttachedReferenceWithPath()
    method testSerializeDetachedResource (line 299) | public function testSerializeDetachedResource()
    method testSerializeDetachedResourceWithPath (line 313) | public function testSerializeDetachedResourceWithPath()
    method testSerializeAttachedResourceDetachesResource (line 327) | public function testSerializeAttachedResourceDetachesResource()
    method testSerializeDetachedReference (line 342) | public function testSerializeDetachedReference()
    method testSerializeDetachedReferenceWithPath (line 357) | public function testSerializeDetachedReferenceWithPath()
    method testSerializeAttachedReferenceDetachesReference (line 372) | public function testSerializeAttachedReferenceDetachesReference()
    method testListChildren (line 388) | public function testListChildren()
    method testListChildrenWithReference (line 409) | public function testListChildrenWithReference()
    method testListChildrenDetached (line 433) | public function testListChildrenDetached()
    method testGetChild (line 443) | public function testGetChild()
    method testGetChildWithReference (line 459) | public function testGetChildWithReference()
    method testGetChildDetached (line 481) | public function testGetChildDetached()
    method testHasChild (line 488) | public function testHasChild()
    method testHasChildWithReference (line 503) | public function testHasChildWithReference()
    method testHasChildDetached (line 521) | public function testHasChildDetached()
    method testHasChildren (line 528) | public function testHasChildren()
    method testHasChildrenWithReference (line 543) | public function testHasChildrenWithReference()
    method testHasChildrenDetached (line 561) | public function testHasChildrenDetached()
    method testGetVersions (line 568) | public function testGetVersions()
    method testGetVersionsDetached (line 586) | public function testGetVersionsDetached()

FILE: tests/Resource/Collection/ArrayResourceCollectionTest.php
  class ArrayResourceCollectionTest (line 20) | class ArrayResourceCollectionTest extends PHPUnit_Framework_TestCase
    method testConstruct (line 22) | public function testConstruct()
    method testConstructFailsIfNoTraversable (line 38) | public function testConstructFailsIfNoTraversable()
    method testConstructFailsIfNoResource (line 46) | public function testConstructFailsIfNoResource()
    method testReplace (line 53) | public function testReplace()
    method testReplaceFailsIfNoTraversable (line 74) | public function testReplaceFailsIfNoTraversable()
    method testReplaceFailsIfNoResource (line 84) | public function testReplaceFailsIfNoResource()
    method testMerge (line 93) | public function testMerge()
    method testMergeFailsIfNoTraversable (line 116) | public function testMergeFailsIfNoTraversable()
    method testMergeFailsIfNoResource (line 126) | public function testMergeFailsIfNoResource()
    method testGetFailsIfNoSuchOffset (line 138) | public function testGetFailsIfNoSuchOffset()
    method testSet (line 145) | public function testSet()
    method testRemove (line 161) | public function testRemove()
    method testHas (line 177) | public function testHas()
    method testClear (line 192) | public function testClear()
    method testAdd (line 205) | public function testAdd()
    method testIsEmpty (line 219) | public function testIsEmpty()
    method testArrayAccess (line 234) | public function testArrayAccess()

FILE: tests/Resource/Collection/FilesystemResourceCollectionTest.php
  class FilesystemResourceCollectionTest (line 24) | class FilesystemResourceCollectionTest extends PHPUnit_Framework_TestCase
    method setUp (line 28) | protected function setUp()
    method testConstruct (line 33) | public function testConstruct()
    method testConstructFailsIfNoTraversable (line 49) | public function testConstructFailsIfNoTraversable()
    method testReplace (line 54) | public function testReplace()
    method testReplaceFailsIfNoTraversable (line 74) | public function testReplaceFailsIfNoTraversable()
    method testAdd (line 81) | public function testAdd()
    method testGetFilesystemPaths (line 95) | public function testGetFilesystemPaths()
    method testGetFilesystemPathsIgnoresNonFilesystemResources (line 108) | public function testGetFilesystemPathsIgnoresNonFilesystemResources()
    method testGetFilesystemPathsIgnoresResourcesWithEmptyFilesystemPaths (line 123) | public function testGetFilesystemPathsIgnoresResourcesWithEmptyFilesys...

FILE: tests/Resource/DirectoryResourceTest.php
  class DirectoryResourceTest (line 23) | class DirectoryResourceTest extends AbstractFilesystemResourceTest
    method setUp (line 29) | protected function setUp()
    method tearDown (line 37) | protected function tearDown()
    method createFilesystemResource (line 45) | protected function createFilesystemResource($filesystemPath, $path = n...
    method getValidFilesystemPath (line 50) | protected function getValidFilesystemPath()
    method getValidFilesystemPath2 (line 55) | protected function getValidFilesystemPath2()
    method getValidFilesystemPath3 (line 60) | protected function getValidFilesystemPath3()
    method getInvalidFilesystemPaths (line 65) | public function getInvalidFilesystemPaths()
    method testFailIfNoDirectory (line 81) | public function testFailIfNoDirectory()
    method testListChildrenDetached (line 86) | public function testListChildrenDetached()
    method testGetChildDetached (line 98) | public function testGetChildDetached()
    method testHasChildDetached (line 105) | public function testHasChildDetached()
    method testHasChildrenDetached (line 116) | public function testHasChildrenDetached()

FILE: tests/Resource/FileResourceTest.php
  class FileResourceTest (line 20) | class FileResourceTest extends AbstractFilesystemResourceTest
    method setUp (line 24) | protected function setUp()
    method createFilesystemResource (line 31) | protected function createFilesystemResource($resourcesystemPath, $path...
    method getValidFilesystemPath (line 36) | protected function getValidFilesystemPath()
    method getValidFilesystemPath2 (line 41) | protected function getValidFilesystemPath2()
    method getValidFilesystemPath3 (line 46) | protected function getValidFilesystemPath3()
    method getInvalidFilesystemPaths (line 51) | public function getInvalidFilesystemPaths()
    method testGetContents (line 64) | public function testGetContents()
    method testListChildren (line 71) | public function testListChildren()
    method testListChildrenWithReference (line 87) | public function testListChildrenWithReference()
    method testListChildrenDetached (line 105) | public function testListChildrenDetached()
    method testGetChild (line 118) | public function testGetChild()
    method testGetChildWithReference (line 134) | public function testGetChildWithReference()
    method testGetChildDetached (line 152) | public function testGetChildDetached()
    method testHasChild (line 159) | public function testHasChild()
    method testHasChildWithReference (line 172) | public function testHasChildWithReference()
    method testHasChildDetached (line 187) | public function testHasChildDetached()
    method testHasChildren (line 194) | public function testHasChildren()
    method testHasChildrenWithReference (line 207) | public function testHasChildrenWithReference()
    method testHasChildrenDetached (line 222) | public function testHasChildrenDetached()

FILE: tests/Resource/GenericResourceTest.php
  class GenericResourceTest (line 19) | class GenericResourceTest extends AbstractResourceTest
    method createResource (line 21) | protected function createResource($path = null)

FILE: tests/Resource/Iterator/ResourceCollectionIteratorTest.php
  class ResourceCollectionIteratorTest (line 24) | class ResourceCollectionIteratorTest extends PHPUnit_Framework_TestCase
    method testDefaultIteration (line 26) | public function testDefaultIteration()
    method testCurrentAsResource (line 62) | public function testCurrentAsResource()
    method testCurrentAsPath (line 101) | public function testCurrentAsPath()
    method testCurrentAsName (line 140) | public function testCurrentAsName()
    method testKeyAsPath (line 179) | public function testKeyAsPath()

FILE: tests/Resource/Iterator/ResourceFilterIteratorTest.php
  class ResourceFilterIteratorTest (line 25) | class ResourceFilterIteratorTest extends PHPUnit_Framework_TestCase
    method setUp (line 32) | protected function setUp()
    method testRejectEmptyPattern (line 57) | public function testRejectEmptyPattern()
    method testFilterPathPrefix (line 64) | public function testFilterPathPrefix()
    method testFilterPathSuffix (line 89) | public function testFilterPathSuffix()
    method testFilterPathRegexImplicit (line 112) | public function testFilterPathRegexImplicit()
    method testFilterPathRegexExplicit (line 134) | public function testFilterPathRegexExplicit()
    method testFilterNamePrefix (line 157) | public function testFilterNamePrefix()

FILE: tests/Resource/LinkResourceTest.php
  class LinkResourceTest (line 20) | class LinkResourceTest extends AbstractResourceTest
    method createResource (line 22) | protected function createResource($path = null)
    method testListChildren (line 27) | public function testListChildren()
    method testListChildrenWithReference (line 48) | public function testListChildrenWithReference()
    method testGetChild (line 72) | public function testGetChild()
    method testGetChildWithReference (line 88) | public function testGetChildWithReference()
    method testHasChild (line 107) | public function testHasChild()
    method testHasChildWithReference (line 122) | public function testHasChildWithReference()
    method testSerializeKeepsTargetPath (line 140) | public function testSerializeKeepsTargetPath()
    method testGetTargetFailsIfNoRepository (line 152) | public function testGetTargetFailsIfNoRepository()
    method testGetChildFailsIfNoRepository (line 161) | public function testGetChildFailsIfNoRepository()

FILE: tests/Resource/TestDirectory.php
  class TestDirectory (line 21) | class TestDirectory extends GenericResource
    method __construct (line 30) | public function __construct($path = null, array $children = array())
    method getChild (line 41) | public function getChild($relPath)
    method hasChild (line 46) | public function hasChild($relPath)
    method hasChildren (line 51) | public function hasChildren()
    method listChildren (line 56) | public function listChildren()
    method getMetadata (line 61) | public function getMetadata()

FILE: tests/Resource/TestFile.php
  class TestFile (line 22) | class TestFile extends GenericResource implements BodyResource
    method __construct (line 30) | public function __construct($path = null, $body = self::BODY)
    method getBody (line 38) | public function getBody()
    method getSize (line 43) | public function getSize()
    method getChild (line 48) | public function getChild($relPath)
    method hasChild (line 53) | public function hasChild($relPath)
    method hasChildren (line 58) | public function hasChildren()
    method listChildren (line 63) | public function listChildren()
    method getMetadata (line 68) | public function getMetadata()

FILE: tests/Resource/TestMetadata.php
  class TestMetadata (line 19) | class TestMetadata extends ResourceMetadata
    method getCreationTime (line 44) | public function getCreationTime()
    method setCreationTime (line 52) | public function setCreationTime($creationTime)
    method getAccessTime (line 60) | public function getAccessTime()
    method setAccessTime (line 68) | public function setAccessTime($accessTime)
    method getModificationTime (line 76) | public function getModificationTime()
    method setModificationTime (line 84) | public function setModificationTime($modificationTime)
    method getSize (line 92) | public function getSize()
    method setSize (line 100) | public function setSize($size)

FILE: tests/StreamWrapper/ResourceStreamWrapperTest.php
  class ResourceStreamWrapperTest (line 26) | class ResourceStreamWrapperTest extends PHPUnit_Framework_TestCase
    method setUp (line 50) | protected function setUp()
    method tearDown (line 84) | protected function tearDown()
    method testOpenNonFile (line 108) | public function testOpenNonFile()
    method provideFilePaths (line 115) | public function provideFilePaths()
    method testRead (line 126) | public function testRead($path)
    method testSeekSet (line 144) | public function testSeekSet($path)
    method testSeekCur (line 160) | public function testSeekCur($path)
    method testSeekInvalidPositiveOffsetLocal (line 173) | public function testSeekInvalidPositiveOffsetLocal()
    method testSeekInvalidPositiveOffsetNonLocal (line 186) | public function testSeekInvalidPositiveOffsetNonLocal()
    method testSeekInvalidNegativeOffset (line 204) | public function testSeekInvalidNegativeOffset($path)
    method testStatLocal (line 219) | public function testStatLocal()
    method testStatNonLocal (line 233) | public function testStatNonLocal()
    method testIsLocal (line 247) | public function testIsLocal()
    method testIsNonLocal (line 256) | public function testIsNonLocal()
    method testExists (line 267) | public function testExists()
    method testSelect (line 281) | public function testSelect($path)
    method testCannotOpenForWriting (line 303) | public function testCannotOpenForWriting($mode)
    method provideWriteModes (line 310) | public function provideWriteModes()
    method testLockIsProhibited (line 329) | public function testLockIsProhibited($path)
    method testTouchExistingIsProhibited (line 341) | public function testTouchExistingIsProhibited($path)
    method testTouchNewIsProhibited (line 357) | public function testTouchNewIsProhibited()
    method testChownIsProhibited (line 374) | public function testChownIsProhibited($path)
    method testChgrpIsProhibited (line 391) | public function testChgrpIsProhibited($path)
    method testChmodIsProhibited (line 408) | public function testChmodIsProhibited($path)
    method testUnlinkIsProhibited (line 425) | public function testUnlinkIsProhibited($path)
    method testRenameIsProhibited (line 435) | public function testRenameIsProhibited()
    method testRmdirIsProhibited (line 445) | public function testRmdirIsProhibited()
    method testMkdirIsProhibited (line 455) | public function testMkdirIsProhibited()
    method testListDirectory (line 462) | public function testListDirectory()
    method testListMultipleDirectories (line 478) | public function testListMultipleDirectories()
    method testOpenNonExistingDirectory (line 498) | public function testOpenNonExistingDirectory()
    method testRegisterCallable (line 505) | public function testRegisterCallable()
    method testCallableNotInvokedIfNotUsed (line 516) | public function testCallableNotInvokedIfNotUsed()
    method testFailIfCallableDoesNotReturnValidRepository (line 535) | public function testFailIfCallableDoesNotReturnValidRepository()
    method testRegisterTwice (line 547) | public function testRegisterTwice()
    method testRegisterFailsIfNotRepoNorCallable (line 556) | public function testRegisterFailsIfNotRepoNorCallable()
    method testRegisterFailsIfSchemeNotString (line 565) | public function testRegisterFailsIfSchemeNotString()
    method testRegisterFailsIfSchemeContainsSpecialChars (line 574) | public function testRegisterFailsIfSchemeContainsSpecialChars()
    method testRegisterFailsIfSchemeDoesNotStartWithLetter (line 583) | public function testRegisterFailsIfSchemeDoesNotStartWithLetter()
    method testUnregisterIsIdempotent (line 588) | public function testUnregisterIsIdempotent()
    method testWrapperShouldNotBeRegisteredManually (line 597) | public function testWrapperShouldNotBeRegisteredManually()

FILE: tests/Uri/UriTest.php
  class UriTest (line 20) | class UriTest extends PHPUnit_Framework_TestCase
    method provideValidUris (line 22) | public function provideValidUris()
    method testParse (line 43) | public function testParse($uri, $parts)
    method provideInvalidUris (line 48) | public function provideInvalidUris()
    method testParseInvalid (line 68) | public function testParseInvalid($uri)
Condensed preview — 115 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (479K chars).
[
  {
    "path": ".composer-auth.json",
    "chars": 262,
    "preview": "{\n    \"github-oauth\": {\n        \"github.com\": \"PLEASE DO NOT USE THIS TOKEN IN YOUR OWN PROJECTS/FORKS\",\n        \"github"
  },
  {
    "path": ".gitattributes",
    "chars": 445,
    "preview": "# Path-based git attributes\n# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html\n\n# Ignore all test and"
  },
  {
    "path": ".gitignore",
    "chars": 23,
    "preview": "/vendor/\ncomposer.lock\n"
  },
  {
    "path": ".styleci.yml",
    "chars": 192,
    "preview": "preset: symfony\n\nenabled:\n    - ordered_use\n    - strict\n\ndisabled:\n    - empty_return\n    - phpdoc_annotation_without_d"
  },
  {
    "path": ".travis.yml",
    "chars": 1178,
    "preview": "language: php\n\nsudo: false\n\ncache:\n  directories:\n    - $HOME/.composer/cache/files\n\nmatrix:\n  include:\n    - php: 5.3\n "
  },
  {
    "path": "CHANGELOG.md",
    "chars": 8305,
    "preview": "Changelog\n=========\n\n* 1.0.0-beta11 (@release_date@)\n\n * added `ResourceBinding`\n * added `ResourceBindingInitializer`\n\n"
  },
  {
    "path": "LICENSE",
    "chars": 1084,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2014 Bernhard Schussek\n\nPermission is hereby granted, free of charge, to any person"
  },
  {
    "path": "README.md",
    "chars": 3958,
    "preview": "The Puli Repository Component\n=============================\n\n[![Build Status](https://travis-ci.org/puli/repository.svg?"
  },
  {
    "path": "appveyor.yml",
    "chars": 1297,
    "preview": "build: false\nplatform: x86\nclone_folder: c:\\projects\\puli\\repository\n\ncache:\n  - c:\\php -> appveyor.yml\n\ninit:\n  - SET P"
  },
  {
    "path": "composer.json",
    "chars": 1452,
    "preview": "{\n    \"name\": \"puli/repository\",\n    \"description\": \"A filesystem-like repository for storing arbitrary resources.\",\n   "
  },
  {
    "path": "phpunit.xml.dist",
    "chars": 434,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<phpunit bootstrap=\"vendor/autoload.php\" colors=\"true\">\n    <testsuites>\n       "
  },
  {
    "path": "res/schema/path-mappings-schema-1.0.json",
    "chars": 3620,
    "preview": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n    \"type\": \"object\",\n    \"description\": \"A list of mappings"
  },
  {
    "path": "src/AbstractEditableRepository.php",
    "chars": 2220,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/AbstractJsonRepository.php",
    "chars": 19226,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/AbstractRepository.php",
    "chars": 1926,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Api/ChangeStream/ChangeStream.php",
    "chars": 1812,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Api/ChangeStream/VersionList.php",
    "chars": 4717,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Api/EditableRepository.php",
    "chars": 2092,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Api/NoVersionFoundException.php",
    "chars": 989,
    "preview": "<?php\n\n/*\n * This file is part of the vendor/project package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * Fo"
  },
  {
    "path": "src/Api/Resource/BodyResource.php",
    "chars": 589,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Api/Resource/FilesystemResource.php",
    "chars": 792,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Api/Resource/PuliResource.php",
    "chars": 7868,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Api/Resource/ResourceMetadata.php",
    "chars": 1442,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Api/ResourceCollection.php",
    "chars": 4055,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Api/ResourceIterator.php",
    "chars": 720,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Api/ResourceNotFoundException.php",
    "chars": 983,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Api/ResourceRepository.php",
    "chars": 4364,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Api/UnsupportedLanguageException.php",
    "chars": 1032,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Api/UnsupportedOperationException.php",
    "chars": 523,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Api/UnsupportedResourceException.php",
    "chars": 563,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/ChangeStream/InMemoryChangeStream.php",
    "chars": 2071,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/ChangeStream/JsonChangeStream.php",
    "chars": 3359,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/ChangeStream/KeyValueStoreChangeStream.php",
    "chars": 2194,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Discovery/ResourceBinding.php",
    "chars": 4015,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Discovery/ResourceBindingInitializer.php",
    "chars": 1633,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/FilesystemRepository.php",
    "chars": 17587,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/InMemoryRepository.php",
    "chars": 7899,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/JsonRepository.php",
    "chars": 36140,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/NullRepository.php",
    "chars": 1909,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/OptimizedJsonRepository.php",
    "chars": 6721,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/RepositoryFactoryException.php",
    "chars": 505,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Resource/AbstractFilesystemResource.php",
    "chars": 1669,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Resource/Collection/ArrayResourceCollection.php",
    "chars": 4839,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Resource/Collection/FilesystemResourceCollection.php",
    "chars": 1500,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Resource/Collection/LazyResourceCollection.php",
    "chars": 7299,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Resource/DirectoryResource.php",
    "chars": 3404,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Resource/FileResource.php",
    "chars": 1546,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Resource/GenericResource.php",
    "chars": 5280,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Resource/Iterator/RecursiveResourceIterator.php",
    "chars": 663,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Resource/Iterator/RecursiveResourceIteratorIterator.php",
    "chars": 1857,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Resource/Iterator/ResourceCollectionIterator.php",
    "chars": 6037,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Resource/Iterator/ResourceFilterIterator.php",
    "chars": 4393,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Resource/LinkResource.php",
    "chars": 2887,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Resource/Metadata/FilesystemMetadata.php",
    "chars": 2122,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/StreamWrapper/ResourceStreamWrapper.php",
    "chars": 16689,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/StreamWrapper/StreamWrapper.php",
    "chars": 1442,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/StreamWrapper/StreamWrapperException.php",
    "chars": 528,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Uri/InvalidUriException.php",
    "chars": 471,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "src/Uri/Uri.php",
    "chars": 2810,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/AbstractEditableRepositoryTest.php",
    "chars": 38229,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/AbstractFilesystemRepositorySymlinkTest.php",
    "chars": 2339,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/AbstractFilesystemRepositoryTest.php",
    "chars": 697,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/AbstractJsonRepositoryTest.php",
    "chars": 7067,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/AbstractRepositoryTest.php",
    "chars": 28197,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Api/ChangeStream/VersionListTest.php",
    "chars": 5460,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/ChangeStream/AbstractChangeStreamTest.php",
    "chars": 6329,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/ChangeStream/InMemoryChangeStreamTest.php",
    "chars": 739,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/ChangeStream/JsonChangeStreamLoadedTest.php",
    "chars": 598,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/ChangeStream/JsonChangeStreamTest.php",
    "chars": 1129,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/ChangeStream/KeyValueStoreChangeStreamTest.php",
    "chars": 1114,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Discovery/Fixtures/SubResourceBinding.php",
    "chars": 461,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Discovery/ResourceBindingInitializerTest.php",
    "chars": 3067,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Discovery/ResourceBindingTest.php",
    "chars": 1769,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/FilesystemRepositoryAbsoluteSymlinkTest.php",
    "chars": 11434,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/FilesystemRepositoryCopyTest.php",
    "chars": 5691,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/FilesystemRepositoryLoadedTest.php",
    "chars": 1939,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/FilesystemRepositoryRelativeSymlinkTest.php",
    "chars": 11294,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Fixtures/dir1/file1",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/Fixtures/dir1/file2",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/Fixtures/dir2/file2",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/Fixtures/dir2/file3",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/Fixtures/dir3/sub/file1",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/Fixtures/dir3/sub/file2",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/Fixtures/dir4/sub/file2",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/Fixtures/dir4/sub/file3",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/Fixtures/dir5/file1",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/Fixtures/dir5/file2",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/Fixtures/dir5/sub/file3",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/Fixtures/dir5/sub/file4",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/InMemoryRepositoryTest.php",
    "chars": 1887,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/JsonRepositoryLoadedTest.php",
    "chars": 616,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/JsonRepositoryTest.php",
    "chars": 2118,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/NullRepositoryTest.php",
    "chars": 2030,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/OptimizedJsonRepositoryLoadedTest.php",
    "chars": 634,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/OptimizedJsonRepositoryTest.php",
    "chars": 1157,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Resource/AbstractFilesystemResourceTest.php",
    "chars": 3204,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Resource/AbstractResourceTest.php",
    "chars": 21809,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Resource/Collection/ArrayResourceCollectionTest.php",
    "chars": 7498,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Resource/Collection/FilesystemResourceCollectionTest.php",
    "chars": 4344,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Resource/DirectoryResourceTest.php",
    "chars": 3590,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Resource/FileResourceTest.php",
    "chars": 6339,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Resource/Fixtures/dir1/file1",
    "chars": 14,
    "preview": "LINE 1\nLINE 2\n"
  },
  {
    "path": "tests/Resource/Fixtures/dir1/file2",
    "chars": 12,
    "preview": "NEW DATANEW\n"
  },
  {
    "path": "tests/Resource/Fixtures/dir2/.dotfile",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/Resource/Fixtures/dir2/file1",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/Resource/Fixtures/file3",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/Resource/GenericResourceTest.php",
    "chars": 563,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Resource/Iterator/ResourceCollectionIteratorTest.php",
    "chars": 8150,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Resource/Iterator/ResourceFilterIteratorTest.php",
    "chars": 6412,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Resource/LinkResourceTest.php",
    "chars": 5274,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Resource/TestDirectory.php",
    "chars": 1407,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Resource/TestFile.php",
    "chars": 1498,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Resource/TestMetadata.php",
    "chars": 1773,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/StreamWrapper/ResourceStreamWrapperTest.php",
    "chars": 17165,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  },
  {
    "path": "tests/Uri/UriTest.php",
    "chars": 1839,
    "preview": "<?php\n\n/*\n * This file is part of the puli/repository package.\n *\n * (c) Bernhard Schussek <bschussek@gmail.com>\n *\n * F"
  }
]

About this extraction

This page contains the full source code of the puli/repository GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 115 files (443.9 KB), approximately 109.1k tokens, and a symbol index with 894 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!