pax_global_header 0000666 0000000 0000000 00000000064 14034700434 0014511 g ustar 00root root 0000000 0000000 52 comment=8434102b404d119dcaf98c8fe19a2655573ac17a contracts-2.4.0/ 0000775 0000000 0000000 00000000000 14034700434 0013514 5 ustar 00root root 0000000 0000000 contracts-2.4.0/.gitignore 0000664 0000000 0000000 00000000042 14034700434 0015500 0 ustar 00root root 0000000 0000000 composer.lock phpunit.xml vendor/ contracts-2.4.0/CHANGELOG.md 0000664 0000000 0000000 00000003240 14034700434 0015324 0 ustar 00root root 0000000 0000000 CHANGELOG ========= 2.4 --- * Add `HttpClientInterface::withOptions()` * Add `TranslatorInterface::getLocale()` 2.3.0 ----- * added `Translation\TranslatableInterface` to enable value-objects to be translated * made `Translation\TranslatorTrait::getLocale()` fallback to intl's `Locale::getDefault()` when available 2.2.0 ----- * added `Service\Attribute\Required` attribute for PHP 8 2.1.3 ----- * fixed compat with PHP 8 2.1.0 ----- * added "symfony/deprecation-contracts" 2.0.1 ----- * added `/json` endpoints to the test mock HTTP server 2.0.0 ----- * bumped minimum PHP version to 7.2 and added explicit type hints * made "psr/event-dispatcher" a required dependency of "symfony/event-dispatcher-contracts" * made "symfony/http-client-contracts" not experimental anymore 1.1.9 ----- * fixed compat with PHP 8 1.1.0 ----- * added `HttpClient` namespace with contracts for implementing flexible HTTP clients * added `EventDispatcherInterface` and `Event` in namespace `EventDispatcher` * added `ServiceProviderInterface` in namespace `Service` 1.0.0 ----- * added `Service\ResetInterface` to provide a way to reset an object to its initial state * added `Translation\TranslatorInterface` and `Translation\TranslatorTrait` * added `Cache` contract to extend PSR-6 with tag invalidation, callback-based computation and stampede protection * added `Service\ServiceSubscriberInterface` to declare the dependencies of a class that consumes a service locator * added `Service\ServiceSubscriberTrait` to implement `Service\ServiceSubscriberInterface` using methods' return types * added `Service\ServiceLocatorTrait` to help implement PSR-11 service locators contracts-2.4.0/Cache/ 0000775 0000000 0000000 00000000000 14034700434 0014517 5 ustar 00root root 0000000 0000000 contracts-2.4.0/Cache/.gitignore 0000664 0000000 0000000 00000000042 14034700434 0016503 0 ustar 00root root 0000000 0000000 vendor/ composer.lock phpunit.xml contracts-2.4.0/Cache/CHANGELOG.md 0000664 0000000 0000000 00000000235 14034700434 0016330 0 ustar 00root root 0000000 0000000 CHANGELOG ========= The changelog is maintained for all Symfony contracts at the following URL: https://github.com/symfony/contracts/blob/main/CHANGELOG.md contracts-2.4.0/Cache/CacheInterface.php 0000664 0000000 0000000 00000004605 14034700434 0020061 0 ustar 00root root 0000000 0000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Cache; use Psr\Cache\CacheItemInterface; use Psr\Cache\InvalidArgumentException; /** * Covers most simple to advanced caching needs. * * @author Nicolas Grekas
*/ interface CacheInterface { /** * Fetches a value from the pool or computes it if not found. * * On cache misses, a callback is called that should return the missing value. * This callback is given a PSR-6 CacheItemInterface instance corresponding to the * requested key, that could be used e.g. for expiration control. It could also * be an ItemInterface instance when its additional features are needed. * * @param string $key The key of the item to retrieve from the cache * @param callable|CallbackInterface $callback Should return the computed value for the given key/item * @param float|null $beta A float that, as it grows, controls the likeliness of triggering * early expiration. 0 disables it, INF forces immediate expiration. * The default (or providing null) is implementation dependent but should * typically be 1.0, which should provide optimal stampede protection. * See https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration * @param array &$metadata The metadata of the cached item {@see ItemInterface::getMetadata()} * * @return mixed The value corresponding to the provided key * * @throws InvalidArgumentException When $key is not valid or when $beta is negative */ public function get(string $key, callable $callback, float $beta = null, array &$metadata = null); /** * Removes an item from the pool. * * @param string $key The key to delete * * @throws InvalidArgumentException When $key is not valid * * @return bool True if the item was successfully removed, false if there was any error */ public function delete(string $key): bool; } contracts-2.4.0/Cache/CacheTrait.php 0000664 0000000 0000000 00000004761 14034700434 0017247 0 ustar 00root root 0000000 0000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Cache; use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\InvalidArgumentException; use Psr\Log\LoggerInterface; // Help opcache.preload discover always-needed symbols class_exists(InvalidArgumentException::class); /** * An implementation of CacheInterface for PSR-6 CacheItemPoolInterface classes. * * @author Nicolas Grekas
*/ trait CacheTrait { /** * {@inheritdoc} */ public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) { return $this->doGet($this, $key, $callback, $beta, $metadata); } /** * {@inheritdoc} */ public function delete(string $key): bool { return $this->deleteItem($key); } private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback, ?float $beta, array &$metadata = null, LoggerInterface $logger = null) { if (0 > $beta = $beta ?? 1.0) { throw new class(sprintf('Argument "$beta" provided to "%s::get()" must be a positive number, %f given.', static::class, $beta)) extends \InvalidArgumentException implements InvalidArgumentException { }; } $item = $pool->getItem($key); $recompute = !$item->isHit() || \INF === $beta; $metadata = $item instanceof ItemInterface ? $item->getMetadata() : []; if (!$recompute && $metadata) { $expiry = $metadata[ItemInterface::METADATA_EXPIRY] ?? false; $ctime = $metadata[ItemInterface::METADATA_CTIME] ?? false; if ($recompute = $ctime && $expiry && $expiry <= ($now = microtime(true)) - $ctime / 1000 * $beta * log(random_int(1, \PHP_INT_MAX) / \PHP_INT_MAX)) { // force applying defaultLifetime to expiry $item->expiresAt(null); $logger && $logger->info('Item "{key}" elected for early recomputation {delta}s before its expiration', [ 'key' => $key, 'delta' => sprintf('%.1f', $expiry - $now), ]); } } if ($recompute) { $save = true; $item->set($callback($item, $save)); if ($save) { $pool->save($item); } } return $item->get(); } } contracts-2.4.0/Cache/CallbackInterface.php 0000664 0000000 0000000 00000001453 14034700434 0020550 0 ustar 00root root 0000000 0000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Cache; use Psr\Cache\CacheItemInterface; /** * Computes and returns the cached value of an item. * * @author Nicolas Grekas
*/ interface CallbackInterface { /** * @param CacheItemInterface|ItemInterface $item The item to compute the value for * @param bool &$save Should be set to false when the value should not be saved in the pool * * @return mixed The computed value for the passed item */ public function __invoke(CacheItemInterface $item, bool &$save); } contracts-2.4.0/Cache/ItemInterface.php 0000664 0000000 0000000 00000003354 14034700434 0017754 0 ustar 00root root 0000000 0000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Cache; use Psr\Cache\CacheException; use Psr\Cache\CacheItemInterface; use Psr\Cache\InvalidArgumentException; /** * Augments PSR-6's CacheItemInterface with support for tags and metadata. * * @author Nicolas Grekas
*/ interface ItemInterface extends CacheItemInterface { /** * References the Unix timestamp stating when the item will expire. */ public const METADATA_EXPIRY = 'expiry'; /** * References the time the item took to be created, in milliseconds. */ public const METADATA_CTIME = 'ctime'; /** * References the list of tags that were assigned to the item, as string[]. */ public const METADATA_TAGS = 'tags'; /** * Reserved characters that cannot be used in a key or tag. */ public const RESERVED_CHARACTERS = '{}()/\@:'; /** * Adds a tag to a cache item. * * Tags are strings that follow the same validation rules as keys. * * @param string|string[] $tags A tag or array of tags * * @return $this * * @throws InvalidArgumentException When $tag is not valid * @throws CacheException When the item comes from a pool that is not tag-aware */ public function tag($tags): self; /** * Returns a list of metadata info that were saved alongside with the cached value. * * See ItemInterface::METADATA_* consts for keys potentially found in the returned array. */ public function getMetadata(): array; } contracts-2.4.0/Cache/LICENSE 0000664 0000000 0000000 00000002051 14034700434 0015522 0 ustar 00root root 0000000 0000000 Copyright (c) 2018-2021 Fabien Potencier 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. contracts-2.4.0/Cache/README.md 0000664 0000000 0000000 00000000510 14034700434 0015772 0 ustar 00root root 0000000 0000000 Symfony Cache Contracts ======================= A set of abstractions extracted out of the Symfony components. Can be used to build on semantics that the Symfony components proved useful - and that already have battle tested implementations. See https://github.com/symfony/contracts/blob/main/README.md for more information. contracts-2.4.0/Cache/TagAwareCacheInterface.php 0000664 0000000 0000000 00000001755 14034700434 0021500 0 ustar 00root root 0000000 0000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Cache; use Psr\Cache\InvalidArgumentException; /** * Allows invalidating cached items using tags. * * @author Nicolas Grekas
*/ interface TagAwareCacheInterface extends CacheInterface { /** * Invalidates cached items using tags. * * When implemented on a PSR-6 pool, invalidation should not apply * to deferred items. Instead, they should be committed as usual. * This allows replacing old tagged values by new ones without * race conditions. * * @param string[] $tags An array of tags to invalidate * * @return bool True on success * * @throws InvalidArgumentException When $tags is not valid */ public function invalidateTags(array $tags); } contracts-2.4.0/Cache/composer.json 0000664 0000000 0000000 00000002001 14034700434 0017232 0 ustar 00root root 0000000 0000000 { "name": "symfony/cache-contracts", "type": "library", "description": "Generic abstractions related to caching", "keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2.5", "psr/cache": "^1.0|^2.0|^3.0" }, "suggest": { "symfony/cache-implementation": "" }, "autoload": { "psr-4": { "Symfony\\Contracts\\Cache\\": "" } }, "minimum-stability": "dev", "extra": { "branch-alias": { "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", "url": "https://github.com/symfony/contracts" } } } contracts-2.4.0/Deprecation/ 0000775 0000000 0000000 00000000000 14034700434 0015751 5 ustar 00root root 0000000 0000000 contracts-2.4.0/Deprecation/.gitignore 0000664 0000000 0000000 00000000042 14034700434 0017735 0 ustar 00root root 0000000 0000000 vendor/ composer.lock phpunit.xml contracts-2.4.0/Deprecation/CHANGELOG.md 0000664 0000000 0000000 00000000235 14034700434 0017562 0 ustar 00root root 0000000 0000000 CHANGELOG ========= The changelog is maintained for all Symfony contracts at the following URL: https://github.com/symfony/contracts/blob/main/CHANGELOG.md contracts-2.4.0/Deprecation/LICENSE 0000664 0000000 0000000 00000002051 14034700434 0016754 0 ustar 00root root 0000000 0000000 Copyright (c) 2020-2021 Fabien Potencier 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. contracts-2.4.0/Deprecation/README.md 0000664 0000000 0000000 00000002264 14034700434 0017234 0 ustar 00root root 0000000 0000000 Symfony Deprecation Contracts ============================= A generic function and convention to trigger deprecation notices. This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices. By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component, the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments. The function requires at least 3 arguments: - the name of the Composer package that is triggering the deprecation - the version of the package that introduced the deprecation - the message of the deprecation - more arguments can be provided: they will be inserted in the message using `printf()` formatting Example: ```php trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin'); ``` This will generate the following message: `Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.` While not necessarily recommended, the deprecation notices can be completely ignored by declaring an empty `function trigger_deprecation() {}` in your application. contracts-2.4.0/Deprecation/composer.json 0000664 0000000 0000000 00000001511 14034700434 0020471 0 ustar 00root root 0000000 0000000 { "name": "symfony/deprecation-contracts", "type": "library", "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.1" }, "autoload": { "files": [ "function.php" ] }, "minimum-stability": "dev", "extra": { "branch-alias": { "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", "url": "https://github.com/symfony/contracts" } } } contracts-2.4.0/Deprecation/function.php 0000664 0000000 0000000 00000001760 14034700434 0020313 0 ustar 00root root 0000000 0000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (!function_exists('trigger_deprecation')) { /** * Triggers a silenced deprecation notice. * * @param string $package The name of the Composer package that is triggering the deprecation * @param string $version The version of the package that introduced the deprecation * @param string $message The message of the deprecation * @param mixed ...$args Values to insert in the message using printf() formatting * * @author Nicolas Grekas
*/
function trigger_deprecation(string $package, string $version, string $message, ...$args): void
{
@trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED);
}
}
contracts-2.4.0/EventDispatcher/ 0000775 0000000 0000000 00000000000 14034700434 0016604 5 ustar 00root root 0000000 0000000 contracts-2.4.0/EventDispatcher/.gitignore 0000664 0000000 0000000 00000000042 14034700434 0020570 0 ustar 00root root 0000000 0000000 vendor/
composer.lock
phpunit.xml
contracts-2.4.0/EventDispatcher/CHANGELOG.md 0000664 0000000 0000000 00000000235 14034700434 0020415 0 ustar 00root root 0000000 0000000 CHANGELOG
=========
The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md
contracts-2.4.0/EventDispatcher/Event.php 0000664 0000000 0000000 00000002747 14034700434 0020410 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\EventDispatcher;
use Psr\EventDispatcher\StoppableEventInterface;
/**
* Event is the base class for classes containing event data.
*
* This class contains no event data. It is used by events that do not pass
* state information to an event handler when an event is raised.
*
* You can call the method stopPropagation() to abort the execution of
* further listeners in your event listener.
*
* @author Guilherme Blanco
*/
class Event implements StoppableEventInterface
{
private $propagationStopped = false;
/**
* {@inheritdoc}
*/
public function isPropagationStopped(): bool
{
return $this->propagationStopped;
}
/**
* Stops the propagation of the event to further event listeners.
*
* If multiple event listeners are connected to the same event, no
* further event listener will be triggered once any trigger calls
* stopPropagation().
*/
public function stopPropagation(): void
{
$this->propagationStopped = true;
}
}
contracts-2.4.0/EventDispatcher/EventDispatcherInterface.php 0000664 0000000 0000000 00000001757 14034700434 0024240 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\EventDispatcher;
use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface;
/**
* Allows providing hooks on domain-specific lifecycles by dispatching events.
*/
interface EventDispatcherInterface extends PsrEventDispatcherInterface
{
/**
* Dispatches an event to all registered listeners.
*
* @param object $event The event to pass to the event handlers/listeners
* @param string|null $eventName The name of the event to dispatch. If not supplied,
* the class of $event should be used instead.
*
* @return object The passed $event MUST be returned
*/
public function dispatch(object $event, string $eventName = null): object;
}
contracts-2.4.0/EventDispatcher/LICENSE 0000664 0000000 0000000 00000002051 14034700434 0017607 0 ustar 00root root 0000000 0000000 Copyright (c) 2018-2021 Fabien Potencier
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.
contracts-2.4.0/EventDispatcher/README.md 0000664 0000000 0000000 00000000534 14034700434 0020065 0 ustar 00root root 0000000 0000000 Symfony EventDispatcher Contracts
=================================
A set of abstractions extracted out of the Symfony components.
Can be used to build on semantics that the Symfony components proved useful - and
that already have battle tested implementations.
See https://github.com/symfony/contracts/blob/main/README.md for more information.
contracts-2.4.0/EventDispatcher/composer.json 0000664 0000000 0000000 00000002052 14034700434 0021325 0 ustar 00root root 0000000 0000000 {
"name": "symfony/event-dispatcher-contracts",
"type": "library",
"description": "Generic abstractions related to dispatching event",
"keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=7.2.5",
"psr/event-dispatcher": "^1"
},
"suggest": {
"symfony/event-dispatcher-implementation": ""
},
"autoload": {
"psr-4": { "Symfony\\Contracts\\EventDispatcher\\": "" }
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "2.4-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
}
}
contracts-2.4.0/HttpClient/ 0000775 0000000 0000000 00000000000 14034700434 0015572 5 ustar 00root root 0000000 0000000 contracts-2.4.0/HttpClient/.gitignore 0000664 0000000 0000000 00000000042 14034700434 0017556 0 ustar 00root root 0000000 0000000 vendor/
composer.lock
phpunit.xml
contracts-2.4.0/HttpClient/CHANGELOG.md 0000664 0000000 0000000 00000000235 14034700434 0017403 0 ustar 00root root 0000000 0000000 CHANGELOG
=========
The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md
contracts-2.4.0/HttpClient/ChunkInterface.php 0000664 0000000 0000000 00000004000 14034700434 0021166 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\HttpClient;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* The interface of chunks returned by ResponseStreamInterface::current().
*
* When the chunk is first, last or timeout, the content MUST be empty.
* When an unchecked timeout or a network error occurs, a TransportExceptionInterface
* MUST be thrown by the destructor unless one was already thrown by another method.
*
* @author Nicolas Grekas
*/
interface ChunkInterface
{
/**
* Tells when the idle timeout has been reached.
*
* @throws TransportExceptionInterface on a network error
*/
public function isTimeout(): bool;
/**
* Tells when headers just arrived.
*
* @throws TransportExceptionInterface on a network error or when the idle timeout is reached
*/
public function isFirst(): bool;
/**
* Tells when the body just completed.
*
* @throws TransportExceptionInterface on a network error or when the idle timeout is reached
*/
public function isLast(): bool;
/**
* Returns a [status code, headers] tuple when a 1xx status code was just received.
*
* @throws TransportExceptionInterface on a network error or when the idle timeout is reached
*/
public function getInformationalStatus(): ?array;
/**
* Returns the content of the response chunk.
*
* @throws TransportExceptionInterface on a network error or when the idle timeout is reached
*/
public function getContent(): string;
/**
* Returns the offset of the chunk in the response body.
*/
public function getOffset(): int;
/**
* In case of error, returns the message that describes it.
*/
public function getError(): ?string;
}
contracts-2.4.0/HttpClient/Exception/ 0000775 0000000 0000000 00000000000 14034700434 0017530 5 ustar 00root root 0000000 0000000 contracts-2.4.0/HttpClient/Exception/ClientExceptionInterface.php 0000664 0000000 0000000 00000000700 14034700434 0025154 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\HttpClient\Exception;
/**
* When a 4xx response is returned.
*
* @author Nicolas Grekas
*/
interface ClientExceptionInterface extends HttpExceptionInterface
{
}
contracts-2.4.0/HttpClient/Exception/DecodingExceptionInterface.php 0000664 0000000 0000000 00000000743 14034700434 0025461 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\HttpClient\Exception;
/**
* When a content-type cannot be decoded to the expected representation.
*
* @author Nicolas Grekas
*/
interface DecodingExceptionInterface extends ExceptionInterface
{
}
contracts-2.4.0/HttpClient/Exception/ExceptionInterface.php 0000664 0000000 0000000 00000000704 14034700434 0024021 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\HttpClient\Exception;
/**
* The base interface for all exceptions in the contract.
*
* @author Nicolas Grekas
*/
interface ExceptionInterface extends \Throwable
{
}
contracts-2.4.0/HttpClient/Exception/HttpExceptionInterface.php 0000664 0000000 0000000 00000001070 14034700434 0024656 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\HttpClient\Exception;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* Base interface for HTTP-related exceptions.
*
* @author Anton Chernikov
*/
interface RedirectionExceptionInterface extends HttpExceptionInterface
{
}
contracts-2.4.0/HttpClient/Exception/ServerExceptionInterface.php 0000664 0000000 0000000 00000000700 14034700434 0025204 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\HttpClient\Exception;
/**
* When a 5xx response is returned.
*
* @author Nicolas Grekas
*/
interface ServerExceptionInterface extends HttpExceptionInterface
{
}
contracts-2.4.0/HttpClient/Exception/TimeoutExceptionInterface.php 0000664 0000000 0000000 00000000702 14034700434 0025366 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\HttpClient\Exception;
/**
* When an idle timeout occurs.
*
* @author Nicolas Grekas
*/
interface TimeoutExceptionInterface extends TransportExceptionInterface
{
}
contracts-2.4.0/HttpClient/Exception/TransportExceptionInterface.php 0000664 0000000 0000000 00000000715 14034700434 0025740 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\HttpClient\Exception;
/**
* When any error happens at the transport level.
*
* @author Nicolas Grekas
*/
interface TransportExceptionInterface extends ExceptionInterface
{
}
contracts-2.4.0/HttpClient/HttpClientInterface.php 0000664 0000000 0000000 00000013331 14034700434 0022203 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\HttpClient;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\Test\HttpClientTestCase;
/**
* Provides flexible methods for requesting HTTP resources synchronously or asynchronously.
*
* @see HttpClientTestCase for a reference test suite
*
* @method static withOptions(array $options) Returns a new instance of the client with new default options
*
* @author Nicolas Grekas
*/
interface HttpClientInterface
{
public const OPTIONS_DEFAULTS = [
'auth_basic' => null, // array|string - an array containing the username as first value, and optionally the
// password as the second one; or string like username:password - enabling HTTP Basic
// authentication (RFC 7617)
'auth_bearer' => null, // string - a token enabling HTTP Bearer authorization (RFC 6750)
'query' => [], // string[] - associative array of query string values to merge with the request's URL
'headers' => [], // iterable|string[]|string[][] - headers names provided as keys or as part of values
'body' => '', // array|string|resource|\Traversable|\Closure - the callback SHOULD yield a string
// smaller than the amount requested as argument; the empty string signals EOF; if
// an array is passed, it is meant as a form payload of field names and values
'json' => null, // mixed - if set, implementations MUST set the "body" option to the JSON-encoded
// value and set the "content-type" header to a JSON-compatible value if it is not
// explicitly defined in the headers option - typically "application/json"
'user_data' => null, // mixed - any extra data to attach to the request (scalar, callable, object...) that
// MUST be available via $response->getInfo('user_data') - not used internally
'max_redirects' => 20, // int - the maximum number of redirects to follow; a value lower than or equal to 0
// means redirects should not be followed; "Authorization" and "Cookie" headers MUST
// NOT follow except for the initial host name
'http_version' => null, // string - defaults to the best supported version, typically 1.1 or 2.0
'base_uri' => null, // string - the URI to resolve relative URLs, following rules in RFC 3986, section 2
'buffer' => true, // bool|resource|\Closure - whether the content of the response should be buffered or not,
// or a stream resource where the response body should be written,
// or a closure telling if/where the response should be buffered based on its headers
'on_progress' => null, // callable(int $dlNow, int $dlSize, array $info) - throwing any exceptions MUST abort
// the request; it MUST be called on DNS resolution, on arrival of headers and on
// completion; it SHOULD be called on upload/download of data and at least 1/s
'resolve' => [], // string[] - a map of host to IP address that SHOULD replace DNS resolution
'proxy' => null, // string - by default, the proxy-related env vars handled by curl SHOULD be honored
'no_proxy' => null, // string - a comma separated list of hosts that do not require a proxy to be reached
'timeout' => null, // float - the idle timeout - defaults to ini_get('default_socket_timeout')
'max_duration' => 0, // float - the maximum execution time for the request+response as a whole;
// a value lower than or equal to 0 means it is unlimited
'bindto' => '0', // string - the interface or the local socket to bind to
'verify_peer' => true, // see https://php.net/context.ssl for the following options
'verify_host' => true,
'cafile' => null,
'capath' => null,
'local_cert' => null,
'local_pk' => null,
'passphrase' => null,
'ciphers' => null,
'peer_fingerprint' => null,
'capture_peer_cert_chain' => false,
'extra' => [], // array - additional options that can be ignored if unsupported, unlike regular options
];
/**
* Requests an HTTP resource.
*
* Responses MUST be lazy, but their status code MUST be
* checked even if none of their public methods are called.
*
* Implementations are not required to support all options described above; they can also
* support more custom options; but in any case, they MUST throw a TransportExceptionInterface
* when an unsupported option is passed.
*
* @throws TransportExceptionInterface When an unsupported option is passed
*/
public function request(string $method, string $url, array $options = []): ResponseInterface;
/**
* Yields responses chunk by chunk as they complete.
*
* @param ResponseInterface|ResponseInterface[]|iterable $responses One or more responses created by the current HTTP client
* @param float|null $timeout The idle timeout before yielding timeout chunks
*/
public function stream($responses, float $timeout = null): ResponseStreamInterface;
}
contracts-2.4.0/HttpClient/LICENSE 0000664 0000000 0000000 00000002051 14034700434 0016575 0 ustar 00root root 0000000 0000000 Copyright (c) 2018-2021 Fabien Potencier
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.
contracts-2.4.0/HttpClient/README.md 0000664 0000000 0000000 00000000522 14034700434 0017050 0 ustar 00root root 0000000 0000000 Symfony HttpClient Contracts
============================
A set of abstractions extracted out of the Symfony components.
Can be used to build on semantics that the Symfony components proved useful - and
that already have battle tested implementations.
See https://github.com/symfony/contracts/blob/main/README.md for more information.
contracts-2.4.0/HttpClient/ResponseInterface.php 0000664 0000000 0000000 00000011371 14034700434 0021725 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\HttpClient;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* A (lazily retrieved) HTTP response.
*
* @author Nicolas Grekas
*/
interface ResponseInterface
{
/**
* Gets the HTTP status code of the response.
*
* @throws TransportExceptionInterface when a network error occurs
*/
public function getStatusCode(): int;
/**
* Gets the HTTP headers of the response.
*
* @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes
*
* @return string[][] The headers of the response keyed by header names in lowercase
*
* @throws TransportExceptionInterface When a network error occurs
* @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
* @throws ClientExceptionInterface On a 4xx when $throw is true
* @throws ServerExceptionInterface On a 5xx when $throw is true
*/
public function getHeaders(bool $throw = true): array;
/**
* Gets the response body as a string.
*
* @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes
*
* @throws TransportExceptionInterface When a network error occurs
* @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
* @throws ClientExceptionInterface On a 4xx when $throw is true
* @throws ServerExceptionInterface On a 5xx when $throw is true
*/
public function getContent(bool $throw = true): string;
/**
* Gets the response body decoded as array, typically from a JSON payload.
*
* @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes
*
* @throws DecodingExceptionInterface When the body cannot be decoded to an array
* @throws TransportExceptionInterface When a network error occurs
* @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
* @throws ClientExceptionInterface On a 4xx when $throw is true
* @throws ServerExceptionInterface On a 5xx when $throw is true
*/
public function toArray(bool $throw = true): array;
/**
* Closes the response stream and all related buffers.
*
* No further chunk will be yielded after this method has been called.
*/
public function cancel(): void;
/**
* Returns info coming from the transport layer.
*
* This method SHOULD NOT throw any ExceptionInterface and SHOULD be non-blocking.
* The returned info is "live": it can be empty and can change from one call to
* another, as the request/response progresses.
*
* The following info MUST be returned:
* - canceled (bool) - true if the response was canceled using ResponseInterface::cancel(), false otherwise
* - error (string|null) - the error message when the transfer was aborted, null otherwise
* - http_code (int) - the last response code or 0 when it is not known yet
* - http_method (string) - the HTTP verb of the last request
* - redirect_count (int) - the number of redirects followed while executing the request
* - redirect_url (string|null) - the resolved location of redirect responses, null otherwise
* - response_headers (array) - an array modelled after the special $http_response_header variable
* - start_time (float) - the time when the request was sent or 0.0 when it's pending
* - url (string) - the last effective URL of the request
* - user_data (mixed) - the value of the "user_data" request option, null if not set
*
* When the "capture_peer_cert_chain" option is true, the "peer_certificate_chain"
* attribute SHOULD list the peer certificates as an array of OpenSSL X.509 resources.
*
* Other info SHOULD be named after curl_getinfo()'s associative return value.
*
* @return mixed An array of all available info, or one of them when $type is
* provided, or null when an unsupported type is requested
*/
public function getInfo(string $type = null);
}
contracts-2.4.0/HttpClient/ResponseStreamInterface.php 0000664 0000000 0000000 00000001050 14034700434 0023072 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\HttpClient;
/**
* Yields response chunks, returned by HttpClientInterface::stream().
*
* @author Nicolas Grekas
*/
interface ResponseStreamInterface extends \Iterator
{
public function key(): ResponseInterface;
public function current(): ChunkInterface;
}
contracts-2.4.0/HttpClient/Test/ 0000775 0000000 0000000 00000000000 14034700434 0016511 5 ustar 00root root 0000000 0000000 contracts-2.4.0/HttpClient/Test/Fixtures/ 0000775 0000000 0000000 00000000000 14034700434 0020322 5 ustar 00root root 0000000 0000000 contracts-2.4.0/HttpClient/Test/Fixtures/web/ 0000775 0000000 0000000 00000000000 14034700434 0021077 5 ustar 00root root 0000000 0000000 contracts-2.4.0/HttpClient/Test/Fixtures/web/index.php 0000664 0000000 0000000 00000011172 14034700434 0022721 0 ustar 00root root 0000000 0000000 $v) {
switch ($k) {
default:
if (0 !== strpos($k, 'HTTP_')) {
continue 2;
}
// no break
case 'SERVER_NAME':
case 'SERVER_PROTOCOL':
case 'REQUEST_URI':
case 'REQUEST_METHOD':
case 'PHP_AUTH_USER':
case 'PHP_AUTH_PW':
$vars[$k] = $v;
}
}
$json = json_encode($vars, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
switch ($vars['REQUEST_URI']) {
default:
exit;
case '/head':
header('Content-Length: '.strlen($json), true);
break;
case '/':
case '/?a=a&b=b':
case 'http://127.0.0.1:8057/':
case 'http://localhost:8057/':
ob_start('ob_gzhandler');
break;
case '/103':
header('HTTP/1.1 103 Early Hints');
header('Link: ; rel=preload; as=style', false);
header('Link: ; rel=preload; as=script', false);
flush();
usleep(1000);
echo "HTTP/1.1 200 OK\r\n";
echo "Date: Fri, 26 May 2017 10:02:11 GMT\r\n";
echo "Content-Length: 13\r\n";
echo "\r\n";
echo 'Here the body';
exit;
case '/404':
header('Content-Type: application/json', true, 404);
break;
case '/404-gzipped':
header('Content-Type: text/plain', true, 404);
ob_start('ob_gzhandler');
@ob_flush();
flush();
usleep(300000);
echo 'some text';
exit;
case '/301':
if ('Basic Zm9vOmJhcg==' === $vars['HTTP_AUTHORIZATION']) {
header('Location: http://127.0.0.1:8057/302', true, 301);
}
break;
case '/301/bad-tld':
header('Location: http://foo.example.', true, 301);
break;
case '/301/invalid':
header('Location: //?foo=bar', true, 301);
break;
case '/302':
if (!isset($vars['HTTP_AUTHORIZATION'])) {
header('Location: http://localhost:8057/', true, 302);
}
break;
case '/302/relative':
header('Location: ..', true, 302);
break;
case '/304':
header('Content-Length: 10', true, 304);
echo '12345';
return;
case '/307':
header('Location: http://localhost:8057/post', true, 307);
break;
case '/length-broken':
header('Content-Length: 1000');
break;
case '/post':
$output = json_encode($_POST + ['REQUEST_METHOD' => $vars['REQUEST_METHOD']], \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
header('Content-Type: application/json', true);
header('Content-Length: '.strlen($output));
echo $output;
exit;
case '/timeout-header':
usleep(300000);
break;
case '/timeout-body':
echo '<1>';
@ob_flush();
flush();
usleep(500000);
echo '<2>';
exit;
case '/timeout-long':
ignore_user_abort(false);
sleep(1);
while (true) {
echo '<1>';
@ob_flush();
flush();
usleep(500);
}
exit;
case '/chunked':
header('Transfer-Encoding: chunked');
echo "8\r\nSymfony \r\n5\r\nis aw\r\n6\r\nesome!\r\n0\r\n\r\n";
exit;
case '/chunked-broken':
header('Transfer-Encoding: chunked');
echo "8\r\nSymfony \r\n5\r\nis aw\r\n6\r\ne";
exit;
case '/gzip-broken':
header('Content-Encoding: gzip');
echo str_repeat('-', 1000);
exit;
case '/max-duration':
ignore_user_abort(false);
while (true) {
echo '<1>';
@ob_flush();
flush();
usleep(500);
}
exit;
case '/json':
header('Content-Type: application/json');
echo json_encode([
'documents' => [
['id' => '/json/1'],
['id' => '/json/2'],
['id' => '/json/3'],
],
]);
exit;
case '/json/1':
case '/json/2':
case '/json/3':
header('Content-Type: application/json');
echo json_encode([
'title' => $vars['REQUEST_URI'],
]);
exit;
}
header('Content-Type: application/json', true);
echo $json;
contracts-2.4.0/HttpClient/Test/HttpClientTestCase.php 0000664 0000000 0000000 00000104006 14034700434 0022735 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\HttpClient\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TimeoutExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* A reference test suite for HttpClientInterface implementations.
*/
abstract class HttpClientTestCase extends TestCase
{
public static function setUpBeforeClass(): void
{
TestHttpServer::start();
}
abstract protected function getHttpClient(string $testCase): HttpClientInterface;
public function testGetRequest()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057', [
'headers' => ['Foo' => 'baR'],
'user_data' => $data = new \stdClass(),
]);
$this->assertSame([], $response->getInfo('response_headers'));
$this->assertSame($data, $response->getInfo()['user_data']);
$this->assertSame(200, $response->getStatusCode());
$info = $response->getInfo();
$this->assertNull($info['error']);
$this->assertSame(0, $info['redirect_count']);
$this->assertSame('HTTP/1.1 200 OK', $info['response_headers'][0]);
$this->assertSame('Host: localhost:8057', $info['response_headers'][1]);
$this->assertSame('http://localhost:8057/', $info['url']);
$headers = $response->getHeaders();
$this->assertSame('localhost:8057', $headers['host'][0]);
$this->assertSame(['application/json'], $headers['content-type']);
$body = json_decode($response->getContent(), true);
$this->assertSame($body, $response->toArray());
$this->assertSame('HTTP/1.1', $body['SERVER_PROTOCOL']);
$this->assertSame('/', $body['REQUEST_URI']);
$this->assertSame('GET', $body['REQUEST_METHOD']);
$this->assertSame('localhost:8057', $body['HTTP_HOST']);
$this->assertSame('baR', $body['HTTP_FOO']);
$response = $client->request('GET', 'http://localhost:8057/length-broken');
$this->expectException(TransportExceptionInterface::class);
$response->getContent();
}
public function testHeadRequest()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('HEAD', 'http://localhost:8057/head', [
'headers' => ['Foo' => 'baR'],
'user_data' => $data = new \stdClass(),
'buffer' => false,
]);
$this->assertSame([], $response->getInfo('response_headers'));
$this->assertSame(200, $response->getStatusCode());
$info = $response->getInfo();
$this->assertSame('HTTP/1.1 200 OK', $info['response_headers'][0]);
$this->assertSame('Host: localhost:8057', $info['response_headers'][1]);
$headers = $response->getHeaders();
$this->assertSame('localhost:8057', $headers['host'][0]);
$this->assertSame(['application/json'], $headers['content-type']);
$this->assertTrue(0 < $headers['content-length'][0]);
$this->assertSame('', $response->getContent());
}
public function testNonBufferedGetRequest()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057', [
'buffer' => false,
'headers' => ['Foo' => 'baR'],
]);
$body = $response->toArray();
$this->assertSame('baR', $body['HTTP_FOO']);
$this->expectException(TransportExceptionInterface::class);
$response->getContent();
}
public function testBufferSink()
{
$sink = fopen('php://temp', 'w+');
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057', [
'buffer' => $sink,
'headers' => ['Foo' => 'baR'],
]);
$body = $response->toArray();
$this->assertSame('baR', $body['HTTP_FOO']);
rewind($sink);
$sink = stream_get_contents($sink);
$this->assertSame($sink, $response->getContent());
}
public function testConditionalBuffering()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057');
$firstContent = $response->getContent();
$secondContent = $response->getContent();
$this->assertSame($firstContent, $secondContent);
$response = $client->request('GET', 'http://localhost:8057', ['buffer' => function () { return false; }]);
$response->getContent();
$this->expectException(TransportExceptionInterface::class);
$response->getContent();
}
public function testReentrantBufferCallback()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057', ['buffer' => function () use (&$response) {
$response->cancel();
return true;
}]);
$this->assertSame(200, $response->getStatusCode());
$this->expectException(TransportExceptionInterface::class);
$response->getContent();
}
public function testThrowingBufferCallback()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057', ['buffer' => function () {
throw new \Exception('Boo.');
}]);
$this->assertSame(200, $response->getStatusCode());
$this->expectException(TransportExceptionInterface::class);
$this->expectExceptionMessage('Boo');
$response->getContent();
}
public function testUnsupportedOption()
{
$client = $this->getHttpClient(__FUNCTION__);
$this->expectException(\InvalidArgumentException::class);
$client->request('GET', 'http://localhost:8057', [
'capture_peer_cert' => 1.0,
]);
}
public function testHttpVersion()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057', [
'http_version' => 1.0,
]);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('HTTP/1.0 200 OK', $response->getInfo('response_headers')[0]);
$body = $response->toArray();
$this->assertSame('HTTP/1.0', $body['SERVER_PROTOCOL']);
$this->assertSame('GET', $body['REQUEST_METHOD']);
$this->assertSame('/', $body['REQUEST_URI']);
}
public function testChunkedEncoding()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/chunked');
$this->assertSame(['chunked'], $response->getHeaders()['transfer-encoding']);
$this->assertSame('Symfony is awesome!', $response->getContent());
$response = $client->request('GET', 'http://localhost:8057/chunked-broken');
$this->expectException(TransportExceptionInterface::class);
$response->getContent();
}
public function testClientError()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/404');
$client->stream($response)->valid();
$this->assertSame(404, $response->getInfo('http_code'));
try {
$response->getHeaders();
$this->fail(ClientExceptionInterface::class.' expected');
} catch (ClientExceptionInterface $e) {
}
try {
$response->getContent();
$this->fail(ClientExceptionInterface::class.' expected');
} catch (ClientExceptionInterface $e) {
}
$this->assertSame(404, $response->getStatusCode());
$this->assertSame(['application/json'], $response->getHeaders(false)['content-type']);
$this->assertNotEmpty($response->getContent(false));
$response = $client->request('GET', 'http://localhost:8057/404');
try {
foreach ($client->stream($response) as $chunk) {
$this->assertTrue($chunk->isFirst());
}
$this->fail(ClientExceptionInterface::class.' expected');
} catch (ClientExceptionInterface $e) {
}
}
public function testIgnoreErrors()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/404');
$this->assertSame(404, $response->getStatusCode());
}
public function testDnsError()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/301/bad-tld');
try {
$response->getStatusCode();
$this->fail(TransportExceptionInterface::class.' expected');
} catch (TransportExceptionInterface $e) {
$this->addToAssertionCount(1);
}
try {
$response->getStatusCode();
$this->fail(TransportExceptionInterface::class.' still expected');
} catch (TransportExceptionInterface $e) {
$this->addToAssertionCount(1);
}
$response = $client->request('GET', 'http://localhost:8057/301/bad-tld');
try {
foreach ($client->stream($response) as $r => $chunk) {
}
$this->fail(TransportExceptionInterface::class.' expected');
} catch (TransportExceptionInterface $e) {
$this->addToAssertionCount(1);
}
$this->assertSame($response, $r);
$this->assertNotNull($chunk->getError());
$this->expectException(TransportExceptionInterface::class);
foreach ($client->stream($response) as $chunk) {
}
}
public function testInlineAuth()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://foo:bar%3Dbar@localhost:8057');
$body = $response->toArray();
$this->assertSame('foo', $body['PHP_AUTH_USER']);
$this->assertSame('bar=bar', $body['PHP_AUTH_PW']);
}
public function testBadRequestBody()
{
$client = $this->getHttpClient(__FUNCTION__);
$this->expectException(TransportExceptionInterface::class);
$response = $client->request('POST', 'http://localhost:8057/', [
'body' => function () { yield []; },
]);
$response->getStatusCode();
}
public function test304()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/304', [
'headers' => ['If-Match' => '"abc"'],
'buffer' => false,
]);
$this->assertSame(304, $response->getStatusCode());
$this->assertSame('', $response->getContent(false));
}
public function testRedirects()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('POST', 'http://localhost:8057/301', [
'auth_basic' => 'foo:bar',
'body' => function () {
yield 'foo=bar';
},
]);
$body = $response->toArray();
$this->assertSame('GET', $body['REQUEST_METHOD']);
$this->assertSame('Basic Zm9vOmJhcg==', $body['HTTP_AUTHORIZATION']);
$this->assertSame('http://localhost:8057/', $response->getInfo('url'));
$this->assertSame(2, $response->getInfo('redirect_count'));
$this->assertNull($response->getInfo('redirect_url'));
$expected = [
'HTTP/1.1 301 Moved Permanently',
'Location: http://127.0.0.1:8057/302',
'Content-Type: application/json',
'HTTP/1.1 302 Found',
'Location: http://localhost:8057/',
'Content-Type: application/json',
'HTTP/1.1 200 OK',
'Content-Type: application/json',
];
$filteredHeaders = array_values(array_filter($response->getInfo('response_headers'), function ($h) {
return \in_array(substr($h, 0, 4), ['HTTP', 'Loca', 'Cont'], true) && 'Content-Encoding: gzip' !== $h;
}));
$this->assertSame($expected, $filteredHeaders);
}
public function testInvalidRedirect()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/301/invalid');
$this->assertSame(301, $response->getStatusCode());
$this->assertSame(['//?foo=bar'], $response->getHeaders(false)['location']);
$this->assertSame(0, $response->getInfo('redirect_count'));
$this->assertNull($response->getInfo('redirect_url'));
$this->expectException(RedirectionExceptionInterface::class);
$response->getHeaders();
}
public function testRelativeRedirects()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/302/relative');
$body = $response->toArray();
$this->assertSame('/', $body['REQUEST_URI']);
$this->assertNull($response->getInfo('redirect_url'));
$response = $client->request('GET', 'http://localhost:8057/302/relative', [
'max_redirects' => 0,
]);
$this->assertSame(302, $response->getStatusCode());
$this->assertSame('http://localhost:8057/', $response->getInfo('redirect_url'));
}
public function testRedirect307()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('POST', 'http://localhost:8057/307', [
'body' => function () {
yield 'foo=bar';
},
'max_redirects' => 0,
]);
$this->assertSame(307, $response->getStatusCode());
$response = $client->request('POST', 'http://localhost:8057/307', [
'body' => 'foo=bar',
]);
$body = $response->toArray();
$this->assertSame(['foo' => 'bar', 'REQUEST_METHOD' => 'POST'], $body);
}
public function testMaxRedirects()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/301', [
'max_redirects' => 1,
'auth_basic' => 'foo:bar',
]);
try {
$response->getHeaders();
$this->fail(RedirectionExceptionInterface::class.' expected');
} catch (RedirectionExceptionInterface $e) {
}
$this->assertSame(302, $response->getStatusCode());
$this->assertSame(1, $response->getInfo('redirect_count'));
$this->assertSame('http://localhost:8057/', $response->getInfo('redirect_url'));
$expected = [
'HTTP/1.1 301 Moved Permanently',
'Location: http://127.0.0.1:8057/302',
'Content-Type: application/json',
'HTTP/1.1 302 Found',
'Location: http://localhost:8057/',
'Content-Type: application/json',
];
$filteredHeaders = array_values(array_filter($response->getInfo('response_headers'), function ($h) {
return \in_array(substr($h, 0, 4), ['HTTP', 'Loca', 'Cont'], true);
}));
$this->assertSame($expected, $filteredHeaders);
}
public function testStream()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057');
$chunks = $client->stream($response);
$result = [];
foreach ($chunks as $r => $chunk) {
if ($chunk->isTimeout()) {
$result[] = 't';
} elseif ($chunk->isLast()) {
$result[] = 'l';
} elseif ($chunk->isFirst()) {
$result[] = 'f';
}
}
$this->assertSame($response, $r);
$this->assertSame(['f', 'l'], $result);
$chunk = null;
$i = 0;
foreach ($client->stream($response) as $chunk) {
++$i;
}
$this->assertSame(1, $i);
$this->assertTrue($chunk->isLast());
}
public function testAddToStream()
{
$client = $this->getHttpClient(__FUNCTION__);
$r1 = $client->request('GET', 'http://localhost:8057');
$completed = [];
$pool = [$r1];
while ($pool) {
$chunks = $client->stream($pool);
$pool = [];
foreach ($chunks as $r => $chunk) {
if (!$chunk->isLast()) {
continue;
}
if ($r1 === $r) {
$r2 = $client->request('GET', 'http://localhost:8057');
$pool[] = $r2;
}
$completed[] = $r;
}
}
$this->assertSame([$r1, $r2], $completed);
}
public function testCompleteTypeError()
{
$client = $this->getHttpClient(__FUNCTION__);
$this->expectException(\TypeError::class);
$client->stream(123);
}
public function testOnProgress()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('POST', 'http://localhost:8057/post', [
'headers' => ['Content-Length' => 14],
'body' => 'foo=0123456789',
'on_progress' => function (...$state) use (&$steps) { $steps[] = $state; },
]);
$body = $response->toArray();
$this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $body);
$this->assertSame([0, 0], \array_slice($steps[0], 0, 2));
$lastStep = \array_slice($steps, -1)[0];
$this->assertSame([57, 57], \array_slice($lastStep, 0, 2));
$this->assertSame('http://localhost:8057/post', $steps[0][2]['url']);
}
public function testPostJson()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('POST', 'http://localhost:8057/post', [
'json' => ['foo' => 'bar'],
]);
$body = $response->toArray();
$this->assertStringContainsString('json', $body['content-type']);
unset($body['content-type']);
$this->assertSame(['foo' => 'bar', 'REQUEST_METHOD' => 'POST'], $body);
}
public function testPostArray()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('POST', 'http://localhost:8057/post', [
'body' => ['foo' => 'bar'],
]);
$this->assertSame(['foo' => 'bar', 'REQUEST_METHOD' => 'POST'], $response->toArray());
}
public function testPostResource()
{
$client = $this->getHttpClient(__FUNCTION__);
$h = fopen('php://temp', 'w+');
fwrite($h, 'foo=0123456789');
rewind($h);
$response = $client->request('POST', 'http://localhost:8057/post', [
'body' => $h,
]);
$body = $response->toArray();
$this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $body);
}
public function testPostCallback()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('POST', 'http://localhost:8057/post', [
'body' => function () {
yield 'foo';
yield '';
yield '=';
yield '0123456789';
},
]);
$this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $response->toArray());
}
public function testCancel()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/timeout-header');
$response->cancel();
$this->expectException(TransportExceptionInterface::class);
$response->getHeaders();
}
public function testInfoOnCanceledResponse()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/timeout-header');
$this->assertFalse($response->getInfo('canceled'));
$response->cancel();
$this->assertTrue($response->getInfo('canceled'));
}
public function testCancelInStream()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/404');
foreach ($client->stream($response) as $chunk) {
$response->cancel();
}
$this->expectException(TransportExceptionInterface::class);
foreach ($client->stream($response) as $chunk) {
}
}
public function testOnProgressCancel()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/timeout-body', [
'on_progress' => function ($dlNow) {
if (0 < $dlNow) {
throw new \Exception('Aborting the request.');
}
},
]);
try {
foreach ($client->stream([$response]) as $chunk) {
}
$this->fail(ClientExceptionInterface::class.' expected');
} catch (TransportExceptionInterface $e) {
$this->assertSame('Aborting the request.', $e->getPrevious()->getMessage());
}
$this->assertNotNull($response->getInfo('error'));
$this->expectException(TransportExceptionInterface::class);
$response->getContent();
}
public function testOnProgressError()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/timeout-body', [
'on_progress' => function ($dlNow) {
if (0 < $dlNow) {
throw new \Error('BUG.');
}
},
]);
try {
foreach ($client->stream([$response]) as $chunk) {
}
$this->fail('Error expected');
} catch (\Error $e) {
$this->assertSame('BUG.', $e->getMessage());
}
$this->assertNotNull($response->getInfo('error'));
$this->expectException(TransportExceptionInterface::class);
$response->getContent();
}
public function testResolve()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://symfony.com:8057/', [
'resolve' => ['symfony.com' => '127.0.0.1'],
]);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame(200, $client->request('GET', 'http://symfony.com:8057/')->getStatusCode());
$response = null;
$this->expectException(TransportExceptionInterface::class);
$client->request('GET', 'http://symfony.com:8057/', ['timeout' => 1]);
}
public function testIdnResolve()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://0-------------------------------------------------------------0.com:8057/', [
'resolve' => ['0-------------------------------------------------------------0.com' => '127.0.0.1'],
]);
$this->assertSame(200, $response->getStatusCode());
$response = $client->request('GET', 'http://Bücher.example:8057/', [
'resolve' => ['xn--bcher-kva.example' => '127.0.0.1'],
]);
$this->assertSame(200, $response->getStatusCode());
}
public function testNotATimeout()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/timeout-header', [
'timeout' => 0.9,
]);
sleep(1);
$this->assertSame(200, $response->getStatusCode());
}
public function testTimeoutOnAccess()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/timeout-header', [
'timeout' => 0.1,
]);
$this->expectException(TransportExceptionInterface::class);
$response->getHeaders();
}
public function testTimeoutIsNotAFatalError()
{
usleep(300000); // wait for the previous test to release the server
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/timeout-body', [
'timeout' => 0.25,
]);
try {
$response->getContent();
$this->fail(TimeoutExceptionInterface::class.' expected');
} catch (TimeoutExceptionInterface $e) {
}
for ($i = 0; $i < 10; ++$i) {
try {
$this->assertSame('<1><2>', $response->getContent());
break;
} catch (TimeoutExceptionInterface $e) {
}
}
if (10 === $i) {
throw $e;
}
}
public function testTimeoutOnStream()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/timeout-body');
$this->assertSame(200, $response->getStatusCode());
$chunks = $client->stream([$response], 0.2);
$result = [];
foreach ($chunks as $r => $chunk) {
if ($chunk->isTimeout()) {
$result[] = 't';
} else {
$result[] = $chunk->getContent();
}
}
$this->assertSame(['<1>', 't'], $result);
$chunks = $client->stream([$response]);
foreach ($chunks as $r => $chunk) {
$this->assertSame('<2>', $chunk->getContent());
$this->assertSame('<1><2>', $r->getContent());
return;
}
$this->fail('The response should have completed');
}
public function testUncheckedTimeoutThrows()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/timeout-body');
$chunks = $client->stream([$response], 0.1);
$this->expectException(TransportExceptionInterface::class);
foreach ($chunks as $r => $chunk) {
}
}
public function testTimeoutWithActiveConcurrentStream()
{
$p1 = TestHttpServer::start(8067);
$p2 = TestHttpServer::start(8077);
$client = $this->getHttpClient(__FUNCTION__);
$streamingResponse = $client->request('GET', 'http://localhost:8067/max-duration');
$blockingResponse = $client->request('GET', 'http://localhost:8077/timeout-body', [
'timeout' => 0.25,
]);
$this->assertSame(200, $streamingResponse->getStatusCode());
$this->assertSame(200, $blockingResponse->getStatusCode());
$this->expectException(TransportExceptionInterface::class);
try {
$blockingResponse->getContent();
} finally {
$p1->stop();
$p2->stop();
}
}
public function testDestruct()
{
$client = $this->getHttpClient(__FUNCTION__);
$start = microtime(true);
$client->request('GET', 'http://localhost:8057/timeout-long');
$client = null;
$duration = microtime(true) - $start;
$this->assertGreaterThan(1, $duration);
$this->assertLessThan(4, $duration);
}
public function testGetContentAfterDestruct()
{
$client = $this->getHttpClient(__FUNCTION__);
try {
$client->request('GET', 'http://localhost:8057/404');
$this->fail(ClientExceptionInterface::class.' expected');
} catch (ClientExceptionInterface $e) {
$this->assertSame('GET', $e->getResponse()->toArray(false)['REQUEST_METHOD']);
}
}
public function testGetEncodedContentAfterDestruct()
{
$client = $this->getHttpClient(__FUNCTION__);
try {
$client->request('GET', 'http://localhost:8057/404-gzipped');
$this->fail(ClientExceptionInterface::class.' expected');
} catch (ClientExceptionInterface $e) {
$this->assertSame('some text', $e->getResponse()->getContent(false));
}
}
public function testProxy()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/', [
'proxy' => 'http://localhost:8057',
]);
$body = $response->toArray();
$this->assertSame('localhost:8057', $body['HTTP_HOST']);
$this->assertMatchesRegularExpression('#^http://(localhost|127\.0\.0\.1):8057/$#', $body['REQUEST_URI']);
$response = $client->request('GET', 'http://localhost:8057/', [
'proxy' => 'http://foo:b%3Dar@localhost:8057',
]);
$body = $response->toArray();
$this->assertSame('Basic Zm9vOmI9YXI=', $body['HTTP_PROXY_AUTHORIZATION']);
}
public function testNoProxy()
{
putenv('no_proxy='.$_SERVER['no_proxy'] = 'example.com, localhost');
try {
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/', [
'proxy' => 'http://localhost:8057',
]);
$body = $response->toArray();
$this->assertSame('HTTP/1.1', $body['SERVER_PROTOCOL']);
$this->assertSame('/', $body['REQUEST_URI']);
$this->assertSame('GET', $body['REQUEST_METHOD']);
} finally {
putenv('no_proxy');
unset($_SERVER['no_proxy']);
}
}
/**
* @requires extension zlib
*/
public function testAutoEncodingRequest()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057');
$this->assertSame(200, $response->getStatusCode());
$headers = $response->getHeaders();
$this->assertSame(['Accept-Encoding'], $headers['vary']);
$this->assertStringContainsString('gzip', $headers['content-encoding'][0]);
$body = $response->toArray();
$this->assertStringContainsString('gzip', $body['HTTP_ACCEPT_ENCODING']);
}
public function testBaseUri()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', '../404', [
'base_uri' => 'http://localhost:8057/abc/',
]);
$this->assertSame(404, $response->getStatusCode());
$this->assertSame(['application/json'], $response->getHeaders(false)['content-type']);
}
public function testQuery()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/?a=a', [
'query' => ['b' => 'b'],
]);
$body = $response->toArray();
$this->assertSame('GET', $body['REQUEST_METHOD']);
$this->assertSame('/?a=a&b=b', $body['REQUEST_URI']);
}
public function testInformationalResponse()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/103');
$this->assertSame('Here the body', $response->getContent());
$this->assertSame(200, $response->getStatusCode());
}
public function testInformationalResponseStream()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/103');
$chunks = [];
foreach ($client->stream($response) as $chunk) {
$chunks[] = $chunk;
}
$this->assertSame(103, $chunks[0]->getInformationalStatus()[0]);
$this->assertSame(['; rel=preload; as=style', '; rel=preload; as=script'], $chunks[0]->getInformationalStatus()[1]['link']);
$this->assertTrue($chunks[1]->isFirst());
$this->assertSame('Here the body', $chunks[2]->getContent());
$this->assertTrue($chunks[3]->isLast());
$this->assertNull($chunks[3]->getInformationalStatus());
$this->assertSame(['date', 'content-length'], array_keys($response->getHeaders()));
$this->assertContains('Link: ; rel=preload; as=style', $response->getInfo('response_headers'));
}
/**
* @requires extension zlib
*/
public function testUserlandEncodingRequest()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057', [
'headers' => ['Accept-Encoding' => 'gzip'],
]);
$headers = $response->getHeaders();
$this->assertSame(['Accept-Encoding'], $headers['vary']);
$this->assertStringContainsString('gzip', $headers['content-encoding'][0]);
$body = $response->getContent();
$this->assertSame("\x1F", $body[0]);
$body = json_decode(gzdecode($body), true);
$this->assertSame('gzip', $body['HTTP_ACCEPT_ENCODING']);
}
/**
* @requires extension zlib
*/
public function testGzipBroken()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/gzip-broken');
$this->expectException(TransportExceptionInterface::class);
$response->getContent();
}
public function testMaxDuration()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057/max-duration', [
'max_duration' => 0.1,
]);
$start = microtime(true);
try {
$response->getContent();
} catch (TransportExceptionInterface $e) {
$this->addToAssertionCount(1);
}
$duration = microtime(true) - $start;
$this->assertLessThan(10, $duration);
}
public function testWithOptions()
{
$client = $this->getHttpClient(__FUNCTION__);
if (!method_exists($client, 'withOptions')) {
$this->markTestSkipped(sprintf('Not implementing "%s::withOptions()" is deprecated.', get_debug_type($client)));
}
$client2 = $client->withOptions(['base_uri' => 'http://localhost:8057/']);
$this->assertNotSame($client, $client2);
$this->assertSame(\get_class($client), \get_class($client2));
$response = $client2->request('GET', '/');
$this->assertSame(200, $response->getStatusCode());
}
}
contracts-2.4.0/HttpClient/Test/TestHttpServer.php 0000664 0000000 0000000 00000002310 14034700434 0022164 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\HttpClient\Test;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;
class TestHttpServer
{
private static $process = [];
public static function start(int $port = 8057)
{
if (isset(self::$process[$port])) {
self::$process[$port]->stop();
} else {
register_shutdown_function(static function () use ($port) {
self::$process[$port]->stop();
});
}
$finder = new PhpExecutableFinder();
$process = new Process(array_merge([$finder->find(false)], $finder->findArguments(), ['-dopcache.enable=0', '-dvariables_order=EGPCS', '-S', '127.0.0.1:'.$port]));
$process->setWorkingDirectory(__DIR__.'/Fixtures/web');
$process->start();
self::$process[$port] = $process;
do {
usleep(50000);
} while (!@fopen('http://127.0.0.1:'.$port, 'r'));
return $process;
}
}
contracts-2.4.0/HttpClient/composer.json 0000664 0000000 0000000 00000001760 14034700434 0020320 0 ustar 00root root 0000000 0000000 {
"name": "symfony/http-client-contracts",
"type": "library",
"description": "Generic abstractions related to HTTP clients",
"keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=7.2.5"
},
"suggest": {
"symfony/http-client-implementation": ""
},
"autoload": {
"psr-4": { "Symfony\\Contracts\\HttpClient\\": "" }
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "2.4-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
}
}
contracts-2.4.0/LICENSE 0000664 0000000 0000000 00000002051 14034700434 0014517 0 ustar 00root root 0000000 0000000 Copyright (c) 2018-2021 Fabien Potencier
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.
contracts-2.4.0/README.md 0000664 0000000 0000000 00000004462 14034700434 0015001 0 ustar 00root root 0000000 0000000 Symfony Contracts
=================
A set of abstractions extracted out of the Symfony components.
Can be used to build on semantics that the Symfony components proved useful - and
that already have battle tested implementations.
Design Principles
-----------------
* contracts are split by domain, each into their own sub-namespaces;
* contracts are small and consistent sets of PHP interfaces, traits, normative
docblocks and reference test suites when applicable;
* all contracts must have a proven implementation to enter this repository;
* they must be backward compatible with existing Symfony components.
Packages that implement specific contracts should list them in the "provide"
section of their "composer.json" file, using the `symfony/*-implementation`
convention (e.g. `"provide": { "symfony/cache-implementation": "1.0" }`).
FAQ
---
### How to use this package?
The abstractions in this package are useful to achieve loose coupling and
interoperability. By using the provided interfaces as type hints, you are able
to reuse any implementations that match their contracts. It could be a Symfony
component, or another one provided by the PHP community at large.
Depending on their semantics, some interfaces can be combined with autowiring to
seamlessly inject a service in your classes.
Others might be useful as labeling interfaces, to hint about a specific behavior
that could be enabled when using autoconfiguration or manual service tagging (or
any other means provided by your framework.)
### How is this different from PHP-FIG's PSRs?
When applicable, the provided contracts are built on top of PHP-FIG's PSRs. But
the group has different goals and different processes. Here, we're focusing on
providing abstractions that are useful on their own while still compatible with
implementations provided by Symfony. Although not the main target, we hope that
the declared contracts will directly or indirectly contribute to the PHP-FIG.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/contracts.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
contracts-2.4.0/Service/ 0000775 0000000 0000000 00000000000 14034700434 0015114 5 ustar 00root root 0000000 0000000 contracts-2.4.0/Service/.gitignore 0000664 0000000 0000000 00000000042 14034700434 0017100 0 ustar 00root root 0000000 0000000 vendor/
composer.lock
phpunit.xml
contracts-2.4.0/Service/Attribute/ 0000775 0000000 0000000 00000000000 14034700434 0017057 5 ustar 00root root 0000000 0000000 contracts-2.4.0/Service/Attribute/Required.php 0000664 0000000 0000000 00000001221 14034700434 0021344 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\Service\Attribute;
/**
* A required dependency.
*
* This attribute indicates that a property holds a required dependency. The annotated property or method should be
* considered during the instantiation process of the containing class.
*
* @author Alexander M. Turek
*/
trait ServiceLocatorTrait
{
private $factories;
private $loading = [];
private $providedTypes;
/**
* @param callable[] $factories
*/
public function __construct(array $factories)
{
$this->factories = $factories;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function has(string $id)
{
return isset($this->factories[$id]);
}
/**
* {@inheritdoc}
*
* @return mixed
*/
public function get(string $id)
{
if (!isset($this->factories[$id])) {
throw $this->createNotFoundException($id);
}
if (isset($this->loading[$id])) {
$ids = array_values($this->loading);
$ids = \array_slice($this->loading, array_search($id, $ids));
$ids[] = $id;
throw $this->createCircularReferenceException($id, $ids);
}
$this->loading[$id] = $id;
try {
return $this->factories[$id]($this);
} finally {
unset($this->loading[$id]);
}
}
/**
* {@inheritdoc}
*/
public function getProvidedServices(): array
{
if (null === $this->providedTypes) {
$this->providedTypes = [];
foreach ($this->factories as $name => $factory) {
if (!\is_callable($factory)) {
$this->providedTypes[$name] = '?';
} else {
$type = (new \ReflectionFunction($factory))->getReturnType();
$this->providedTypes[$name] = $type ? ($type->allowsNull() ? '?' : '').($type instanceof \ReflectionNamedType ? $type->getName() : $type) : '?';
}
}
}
return $this->providedTypes;
}
private function createNotFoundException(string $id): NotFoundExceptionInterface
{
if (!$alternatives = array_keys($this->factories)) {
$message = 'is empty...';
} else {
$last = array_pop($alternatives);
if ($alternatives) {
$message = sprintf('only knows about the "%s" and "%s" services.', implode('", "', $alternatives), $last);
} else {
$message = sprintf('only knows about the "%s" service.', $last);
}
}
if ($this->loading) {
$message = sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $message);
} else {
$message = sprintf('Service "%s" not found: the current service locator %s', $id, $message);
}
return new class($message) extends \InvalidArgumentException implements NotFoundExceptionInterface {
};
}
private function createCircularReferenceException(string $id, array $path): ContainerExceptionInterface
{
return new class(sprintf('Circular reference detected for service "%s", path: "%s".', $id, implode(' -> ', $path))) extends \RuntimeException implements ContainerExceptionInterface {
};
}
}
contracts-2.4.0/Service/ServiceProviderInterface.php 0000664 0000000 0000000 00000002272 14034700434 0022564 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\Service;
use Psr\Container\ContainerInterface;
/**
* A ServiceProviderInterface exposes the identifiers and the types of services provided by a container.
*
* @author Nicolas Grekas
* @author Mateusz Sip
*/
interface ServiceSubscriberInterface
{
/**
* Returns an array of service types required by such instances, optionally keyed by the service names used internally.
*
* For mandatory dependencies:
*
* * ['logger' => 'Psr\Log\LoggerInterface'] means the objects use the "logger" name
* internally to fetch a service which must implement Psr\Log\LoggerInterface.
* * ['loggers' => 'Psr\Log\LoggerInterface[]'] means the objects use the "loggers" name
* internally to fetch an iterable of Psr\Log\LoggerInterface instances.
* * ['Psr\Log\LoggerInterface'] is a shortcut for
* * ['Psr\Log\LoggerInterface' => 'Psr\Log\LoggerInterface']
*
* otherwise:
*
* * ['logger' => '?Psr\Log\LoggerInterface'] denotes an optional dependency
* * ['loggers' => '?Psr\Log\LoggerInterface[]'] denotes an optional iterable dependency
* * ['?Psr\Log\LoggerInterface'] is a shortcut for
* * ['Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface']
*
* @return string[] The required service types, optionally keyed by service names
*/
public static function getSubscribedServices();
}
contracts-2.4.0/Service/ServiceSubscriberTrait.php 0000664 0000000 0000000 00000003507 14034700434 0022262 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\Service;
use Psr\Container\ContainerInterface;
/**
* Implementation of ServiceSubscriberInterface that determines subscribed services from
* private method return types. Service ids are available as "ClassName::methodName".
*
* @author Kevin Bond
*/
interface TranslatableInterface
{
public function trans(TranslatorInterface $translator, string $locale = null): string;
}
contracts-2.4.0/Translation/TranslatorInterface.php 0000664 0000000 0000000 00000005012 14034700434 0022473 0 ustar 00root root 0000000 0000000
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\Translation;
/**
* @author Fabien Potencier